= [.day]
+ let components = calendar.dateComponents(unitFlags, from: date, to: Date())
+ if let numberOfDays = components.day {
+ switch numberOfDays {
+ case 0:
+ let relativeTime = (date as NSDate).wmf_fullyLocalizedRelativeDateStringFromLocalDateToNow()
+ return relativeTime
+ default:
+ let shortTime = timeDateFormatter.string(from: date)
+ return shortTime
+ }
+ }
+ }
+ }
+
+ return nil
+ }
+}
+
+// MARK: SectionHeader
+
+public extension ArticleAsLivingDocViewModel {
+
+ class SectionHeader: Hashable {
+ public let title: String
+ public let subtitleTimestampDisplay: String
+ public let timestamp: Date
+ public let dateRange: DateInterval?
+ public var typedEvents: [TypedEvent]
+
+ private let sectionTimestampIdentifier: String
+
+ private static let calendar: Calendar = Calendar.current
+
+ private static let relativeDateFormatter: DateFormatter = {
+ let formatter = DateFormatter()
+ formatter.doesRelativeDateFormatting = true
+ formatter.timeStyle = .none
+ formatter.dateStyle = .short
+ return formatter
+ }()
+
+ private static let dayMonthYearFormatter: DateFormatter = {
+ let formatter = DateFormatter()
+ formatter.setLocalizedDateFormatFromTemplate("MMMM d, yyyy")
+ return formatter
+ }()
+
+ private static let dayMonthFormatter: DateFormatter = {
+ let formatter = DateFormatter()
+ formatter.setLocalizedDateFormatFromTemplate("MMMM d")
+ return formatter
+ }()
+
+ public init(timestamp: Date, typedEvents: [TypedEvent], subtitleDateFormatter: DateFormatter, dateRange: DateInterval? = nil) {
+
+ // If today or yesterday, show friendly localized relative format ("Today", "Yesterday")
+ func relativelyFormat(date: Date) -> String {
+ let nsTimestamp = timestamp as NSDate
+ if SectionHeader.calendar.isDateInToday(date) || SectionHeader.calendar.isDateInYesterday(date) {
+ return SectionHeader.relativeDateFormatter.string(from: date)
+ } else {
+ return nsTimestamp.wmf_fullyLocalizedRelativeDateStringFromLocalDateToNow()
+ }
+ }
+
+ self.title = relativelyFormat(date: timestamp)
+
+ if let dateRange = dateRange {
+ var dateRangeStrings: [String] = []
+
+ let startDate = dateRange.start
+ let endDate = dateRange.end
+
+ if SectionHeader.calendar.isDateInToday(endDate) || SectionHeader.calendar.isDateInYesterday(endDate) {
+ let endString = relativelyFormat(date: endDate)
+ let startString = SectionHeader.dayMonthYearFormatter.string(from: startDate)
+ dateRangeStrings.append(contentsOf: [endString, startString])
+ } else {
+ let endString = SectionHeader.dayMonthFormatter.string(from: endDate)
+ let startString = SectionHeader.dayMonthYearFormatter.string(from: startDate)
+ dateRangeStrings.append(contentsOf: [endString, startString])
+ }
+
+ self.subtitleTimestampDisplay = dateRangeStrings.joined(separator: " - ")
+ } else {
+ self.subtitleTimestampDisplay = SectionHeader.dayMonthYearFormatter.string(from: timestamp)
+ }
+
+ self.dateRange = dateRange
+ self.timestamp = timestamp
+ self.typedEvents = typedEvents
+ self.sectionTimestampIdentifier = subtitleDateFormatter.string(from: timestamp)
+ }
+
+ public static func == (lhs: ArticleAsLivingDocViewModel.SectionHeader, rhs: ArticleAsLivingDocViewModel.SectionHeader) -> Bool {
+ return lhs.sectionTimestampIdentifier == rhs.sectionTimestampIdentifier
+ }
+
+ public func hash(into hasher: inout Hasher) {
+ hasher.combine(sectionTimestampIdentifier)
+ hasher.combine(typedEvents)
+ }
+
+ // MARK: - Helpers
+
+ public var containsOnlySmallEvents: Bool {
+ typedEvents.count == 1 && typedEvents.allSatisfy { event in event.isSmall }
+ }
+
+ }
+}
+
+// MARK: Events
+
+public extension ArticleAsLivingDocViewModel {
+
+ enum TypedEvent: Hashable {
+ case small(Event.Small)
+ case large(Event.Large)
+
+ public static func == (lhs: ArticleAsLivingDocViewModel.TypedEvent, rhs: ArticleAsLivingDocViewModel.TypedEvent) -> Bool {
+ switch lhs {
+ case .large(let leftLargeEvent):
+ switch rhs {
+ case .large(let rightLargeEvent):
+ return leftLargeEvent == rightLargeEvent
+ default:
+ return false
+ }
+ case .small(let leftSmallEvent):
+ switch rhs {
+ case .small(let rightSmallEvent):
+ return leftSmallEvent == rightSmallEvent
+ default:
+ return false
+ }
+
+ }
+ }
+
+ public func hash(into hasher: inout Hasher) {
+ switch self {
+ case .small(let smallEvent):
+ smallEvent.smallChanges.forEach { hasher.combine($0.revId) }
+ case .large(let largeEvent):
+ hasher.combine(largeEvent.revId)
+ hasher.combine(largeEvent.wereThanksSent)
+ }
+ }
+
+ // MARK: - Helpers
+
+ public var isSmall: Bool {
+ switch self {
+ case .small:
+ return true
+ default:
+ return false
+ }
+ }
+
+ public var smallChanges: [SignificantEvents.Event.Small] {
+ switch self {
+ case .small(let small):
+ return small.smallChanges
+ default:
+ return []
+ }
+ }
+ }
+}
+
+public extension ArticleAsLivingDocViewModel {
+
+ struct Event {
+
+ public class Small: Equatable {
+
+ private var lastTraitCollection: UITraitCollection?
+ private var lastTheme: Theme?
+ public lazy var eventDescription = {
+ return String.localizedStringWithFormat(
+ CommonStrings.smallChangeDescription,
+ smallChanges.count)
+ }()
+ public let smallChanges: [SignificantEvents.Event.Small]
+ public var loggingPosition = 0
+
+ public init?(typedEvents: [SignificantEvents.TypedEvent]) {
+ var smallChanges: [SignificantEvents.Event.Small] = []
+ for event in typedEvents {
+ switch event {
+ case .small(let smallChange):
+ smallChanges.append(smallChange)
+ default:
+ return nil
+ }
+ }
+
+ guard smallChanges.count > 0 else {
+ return nil
+ }
+
+ self.smallChanges = smallChanges
+ }
+
+ public init(smallChanges: [SignificantEvents.Event.Small]) {
+ self.smallChanges = smallChanges
+ }
+
+ public static func == (lhs: ArticleAsLivingDocViewModel.Event.Small, rhs: ArticleAsLivingDocViewModel.Event.Small) -> Bool {
+ return lhs.smallChanges == rhs.smallChanges
+ }
+
+ // Only used in the html portion of the feature
+ func timestampForDisplay() -> String? {
+
+ guard let timestampString = smallChanges.first?.timestampString else {
+ return nil
+ }
+
+ let displayTimestamp = ArticleAsLivingDocViewModel.displayTimestamp(timestampString: timestampString, fullyRelative: true)
+
+ return displayTimestamp
+ }
+ }
+
+ public class Large: Equatable {
+
+ public enum ChangeDetail {
+ case snippet(Snippet) // use for a basic horizontally scrolling snippet cell (will contain talk page topic snippets, added text snippets, article description updated snippets)
+ case reference(Reference)
+ }
+
+ public struct Snippet {
+ public let description: NSAttributedString
+ }
+
+ public struct Reference {
+ public let type: String
+ public let description: NSAttributedString
+ public let accessDateYearDisplay: String?
+
+ init?(type: String, description: NSAttributedString?, accessDateYearDisplay: String?) {
+ guard let description = description else {
+ return nil
+ }
+
+ self.type = type
+ self.description = description
+ self.accessDateYearDisplay = accessDateYearDisplay
+ }
+ }
+
+ public enum UserType {
+ case standard
+ case anonymous
+ case bot
+ }
+
+ public enum ButtonsToDisplay {
+ case thankAndViewChanges(userId: UInt, revisionId: UInt)
+ case viewDiscussion(sectionName: String?)
+ }
+
+ public let typedEvent: SignificantEvents.TypedEvent
+
+ private var lastTraitCollection: UITraitCollection?
+ private var lastTheme: Theme?
+
+ private(set) var eventDescription: NSAttributedString?
+ private(set) var sideScrollingCollectionViewHeight: CGFloat?
+ private(set) var changeDetails: [ChangeDetail]?
+ private(set) var displayTimestamp: String?
+ private(set) var userInfo: NSAttributedString?
+ let userId: UInt
+ public let userType: UserType
+ public let buttonsToDisplay: ButtonsToDisplay
+ public let revId: UInt
+ public let parentId: UInt
+ public var wereThanksSent = false
+ public var loggingPosition = 0
+
+ init?(typedEvent: SignificantEvents.TypedEvent) {
+
+ let userGroups: [String]?
+ switch typedEvent {
+ case .newTalkPageTopic(let newTalkPageTopic):
+ self.userId = newTalkPageTopic.userId
+ userGroups = newTalkPageTopic.userGroups
+
+ if let talkPageSection = newTalkPageTopic.section {
+ self.buttonsToDisplay = .viewDiscussion(sectionName: Self.sectionTitleWithWikitextAndHtmlStripped(originalTitle: talkPageSection))
+ } else {
+ self.buttonsToDisplay = .viewDiscussion(sectionName: nil)
+ }
+
+ case .large(let largeChange):
+ self.userId = largeChange.userId
+ userGroups = largeChange.userGroups
+ self.buttonsToDisplay = .thankAndViewChanges(userId: largeChange.userId, revisionId: largeChange.revId)
+ case .vandalismRevert(let vandalismRevert):
+ self.userId = vandalismRevert.userId
+ userGroups = vandalismRevert.userGroups
+ self.buttonsToDisplay = .thankAndViewChanges(userId: vandalismRevert.userId, revisionId: vandalismRevert.revId)
+ case .small:
+ return nil
+ }
+
+ if let userGroups = userGroups,
+ userGroups.contains("bot") {
+ userType = .bot
+ } else if self.userId == 0 {
+ userType = .anonymous
+ } else {
+ userType = .standard
+ }
+
+ self.typedEvent = typedEvent
+ switch typedEvent {
+ case .large(let largeChange):
+ revId = largeChange.revId
+ parentId = largeChange.parentId
+ case .newTalkPageTopic(let newTalkPageTopic):
+ revId = newTalkPageTopic.revId
+ parentId = newTalkPageTopic.parentId
+ case .vandalismRevert(let vandalismRevert):
+ revId = vandalismRevert.revId
+ parentId = vandalismRevert.parentId
+ default:
+ assertionFailure("Shouldn't happen")
+ revId = 0
+ parentId = 0
+ }
+ }
+
+ public static func == (lhs: ArticleAsLivingDocViewModel.Event.Large, rhs: ArticleAsLivingDocViewModel.Event.Large) -> Bool {
+ return lhs.revId == rhs.revId && lhs.wereThanksSent == rhs.wereThanksSent
+ }
+
+ }
+
+ }
+}
+
+// MARK: Large Event Type Helper methods
+
+public extension ArticleAsLivingDocViewModel.Event.Large {
+
+ func articleInsertHtmlSnippet(isFirst: Bool = false, isLast: Bool = false, indexPath: IndexPath) -> String? {
+ guard let timestampForDisplay = self.fullyRelativeTimestampForDisplay(),
+ let userInfo = userInfoHtmlSnippet() else {
+ return nil
+ }
+
+ let liElementIdName = isFirst ? "significant-changes-first-list" : isLast ? "significant-changes-last-list" : "significant-changes-list"
+
+ let lastUserInfoIdAdditions = isLast ? " id='significant-changes-userInfo-last'" : ""
+
+ return "\(timestampForDisplay)
\(eventDescription ?? NSAttributedString(string: ""))
\(userInfo)
"
+ }
+
+ private var htmlSignificantEventsLinkEndingTag: String {
+ return ""
+ }
+
+ // if trait collection or theme is different from the last time attributed strings were generated,
+ // reset to nil to trigger generation again the next time it's requested
+ func resetAttributedStringsIfNeededWithTraitCollection(_ traitCollection: UITraitCollection, theme: Theme) {
+ if let lastTraitCollection = lastTraitCollection,
+ let lastTheme = lastTheme,
+ lastTraitCollection != traitCollection || lastTheme != theme {
+ eventDescription = nil
+ sideScrollingCollectionViewHeight = nil
+ changeDetails = nil
+ userInfo = nil
+ Self.heightForThreeLineSnippet = nil
+ Self.heightForReferenceTitle = nil
+ }
+
+ lastTraitCollection = traitCollection
+ lastTheme = theme
+ }
+
+ struct IndividualDescription {
+ let priority: Int // used for sorting
+ let text: String
+ }
+
+ private func individualDescriptionsForTypedChanges(_ typedChanges: [SignificantEvents.TypedChange]) -> [IndividualDescription] {
+
+ var descriptions: [IndividualDescription] = []
+ var numReferences = 0
+ for typedChange in typedChanges {
+ switch typedChange {
+ case .addedText(let addedText):
+ let countNumber = NSNumber(value: addedText.characterCount)
+ let characterCount = "\(NumberFormatter.localizedThousandsStringFromNumber(countNumber).localizedLowercase) " + String.localizedStringWithFormat(CommonStrings.charactersTextDescription, addedText.characterCount)
+ let description = String.localizedStringWithFormat(CommonStrings.addedTextDescription, characterCount)
+ descriptions.append(IndividualDescription(priority: 1, text: description))
+ case .deletedText(let deletedText):
+ let countNumber = NSNumber(value: deletedText.characterCount)
+ let characterCount = "\(NumberFormatter.localizedThousandsStringFromNumber(countNumber).localizedLowercase) " + String.localizedStringWithFormat(CommonStrings.charactersTextDescription, deletedText.characterCount)
+ let description = String.localizedStringWithFormat(CommonStrings.deletedTextDescription, characterCount)
+ descriptions.append(IndividualDescription(priority: 2, text: description))
+ case .newTemplate(let newTemplate):
+ var numArticleDescriptions = 0
+ for template in newTemplate.typedTemplates {
+ switch template {
+ case .articleDescription:
+ numArticleDescriptions += 1
+ case .bookCitation,
+ .journalCitation,
+ .newsCitation,
+ .websiteCitation:
+ numReferences += 1
+ }
+ }
+
+ if numArticleDescriptions > 0 {
+ let description = CommonStrings.articleDescriptionUpdatedDescription
+ descriptions.append(IndividualDescription(priority: 3, text: description))
+ }
+ }
+ }
+
+ if descriptions.count == 0 {
+ switch numReferences {
+ case 0:
+ break
+ case 1:
+ let description = CommonStrings.singleReferenceAddedDescription
+ descriptions.append(IndividualDescription(priority: 0, text: description))
+ default:
+ let description = CommonStrings.multipleReferencesAddedDescription
+ descriptions.append(IndividualDescription(priority: 0, text: description))
+ }
+ } else {
+ if numReferences > 0 {
+ let description = String.localizedStringWithFormat(CommonStrings.numericalMultipleReferencesAddedDescription, numReferences)
+ descriptions.append(IndividualDescription(priority: 0, text: description))
+ }
+ }
+
+ return descriptions
+ }
+
+ private func sectionsSet() -> Set {
+ let set: Set
+ switch typedEvent {
+ case .newTalkPageTopic:
+ set = Set()
+ case .vandalismRevert(let vandalismRevert):
+ set = Set(vandalismRevert.sections)
+ case .large(let largeChange):
+ var sections: [String] = []
+ for typedChange in largeChange.typedChanges {
+ switch typedChange {
+ case .addedText(let addedTextChange):
+ sections.append(contentsOf: addedTextChange.sections)
+ case .deletedText(let deletedTextChange):
+ sections.append(contentsOf: deletedTextChange.sections)
+ case .newTemplate(let newTemplate):
+ sections.append(contentsOf: newTemplate.sections)
+ }
+ }
+
+ set = Set(sections)
+ case .small:
+ assertionFailure("This shouldn't happen")
+ set = Set()
+ }
+
+ // strip == signs from all section titles
+ let finalSet = set.map { Self.sectionTitleWithWikitextAndHtmlStripped(originalTitle: $0) }
+
+ return Set(finalSet)
+ }
+
+ // remove one or more equal signs and zero or more spaces on either side of the title text
+ // also removing html for display and potential javascript injection issues - https://phabricator.wikimedia.org/T268201
+ private static func sectionTitleWithWikitextAndHtmlStripped(originalTitle: String) -> String {
+ var loopTitle = originalTitle.removingHTML
+
+ let regex = "^=+\\s*|\\s*=+$"
+ var maybeMatch = loopTitle.range(of: regex, options: .regularExpression)
+ while let match = maybeMatch {
+ loopTitle.removeSubrange(match)
+ maybeMatch = loopTitle.range(of: regex, options: .regularExpression)
+ }
+
+ return loopTitle
+ }
+
+ private func localizedStringFromSections(sections: [String]) -> String? {
+ var localizedString: String
+ switch sections.count {
+ case 0:
+ return nil
+ case 1:
+ let firstSection = sections[0]
+ localizedString = String.localizedStringWithFormat(CommonStrings.oneSectionDescription, firstSection)
+ case 2:
+ let firstSection = sections[0]
+ let secondSection = sections[1]
+ localizedString = String.localizedStringWithFormat(CommonStrings.twoSectionsDescription, firstSection, secondSection)
+ default:
+ localizedString = String.localizedStringWithFormat(CommonStrings.manySectionsDescription, sections.count)
+ }
+
+ return " " + localizedString
+ }
+
+ private func localizedSectionHtmlSnippet(sectionsSet: Set) -> String? {
+
+ let sections = Array(sectionsSet)
+ guard let localizedString = localizedStringFromSections(sections: sections) else {
+ return nil
+ }
+
+ let mutableLocalizedString = NSMutableString(string: localizedString)
+
+ var ranges: [NSRange] = []
+ for section in sections {
+ let rangeOfSection = (localizedString as NSString).range(of: section)
+ let rangeValid = rangeOfSection.location != NSNotFound && rangeOfSection.location + rangeOfSection.length <= localizedString.count
+ if rangeValid {
+ ranges.append(rangeOfSection)
+ }
+ }
+
+ var offset = 0
+ for range in ranges {
+ let italicStart = ""
+ let italicEnd = ""
+ mutableLocalizedString.insert(italicStart, at: range.location + offset)
+ offset += italicStart.count
+ mutableLocalizedString.insert(italicEnd, at: range.location + range.length + offset)
+ offset += italicEnd.count
+ }
+
+ if let returnString = mutableLocalizedString.copy() as? NSString {
+ return returnString as String
+ } else {
+ assertionFailure("This shouldn't happen")
+ return nil
+ }
+ }
+
+ private func localizedSectionAttributedString(sectionsSet: Set, traitCollection: UITraitCollection, theme: Theme) -> NSAttributedString? {
+ let sections = Array(sectionsSet)
+ guard let localizedString = localizedStringFromSections(sections: sections) else {
+ return nil
+ }
+
+ let font = UIFont.wmf_font(.body, compatibleWithTraitCollection: traitCollection)
+ let italicFont = UIFont.wmf_font(.italicBody, compatibleWithTraitCollection: traitCollection)
+ let attributes = [NSAttributedString.Key.font: font,
+ NSAttributedString.Key.foregroundColor: theme.colors.primaryText]
+
+ var ranges: [NSRange] = []
+ for section in sections {
+ let rangeOfSection = (localizedString as NSString).range(of: section)
+ let rangeValid = rangeOfSection.location != NSNotFound && rangeOfSection.location + rangeOfSection.length <= localizedString.count
+ if rangeValid {
+ ranges.append(rangeOfSection)
+ }
+ }
+
+ let mutableAttributedString = NSMutableAttributedString(string: localizedString, attributes: attributes)
+
+ for range in ranges {
+ mutableAttributedString.addAttribute(NSAttributedString.Key.font, value: italicFont, range: range)
+ }
+
+ guard let attributedString = mutableAttributedString.copy() as? NSAttributedString else {
+ assertionFailure("This shouldn't happen")
+ return NSAttributedString(string: localizedString, attributes: attributes)
+ }
+
+ return attributedString
+ }
+
+ static let sideScrollingCellPadding = UIEdgeInsets(top: 17, left: 15, bottom: 17, right: 15)
+ static let sideScrollingCellWidth: CGFloat = 250
+ static var availableSideScrollingCellWidth: CGFloat = {
+ return sideScrollingCellWidth - sideScrollingCellPadding.left - sideScrollingCellPadding.right
+ }()
+
+ private static let changeDetailDescriptionTextStyle = DynamicTextStyle.subheadline
+ private static let changeDetailDescriptionTextStyleItalic = DynamicTextStyle.italicSubheadline
+ private static let changeDetailDescriptionFontWeight = UIFont.Weight.regular
+
+ static let changeDetailReferenceTitleStyle = DynamicTextStyle.semiboldSubheadline
+ static let changeDetailReferenceTitleDescriptionSpacing: CGFloat = 13
+ static let additionalPointsForShadow: CGFloat = 16
+
+ @discardableResult func calculateSideScrollingCollectionViewHeightForTraitCollection(_ traitCollection: UITraitCollection, theme: Theme) -> CGFloat {
+
+ if let sideScrollingCollectionViewHeight = sideScrollingCollectionViewHeight {
+ return sideScrollingCollectionViewHeight
+ }
+
+ let changeDetails = changeDetailsForTraitCollection(traitCollection, theme: theme)
+
+ let tallestSnippetContentHeight: CGFloat = calculateTallestSnippetContentHeightInChangeDetails(changeDetails: changeDetails)
+ let tallestReferenceChangeDetailHeight: CGFloat = calculateTallestReferenceContentHeightInChangeDetails(changeDetails: changeDetails, traitCollection: traitCollection, theme: theme)
+
+ let maxContentHeight = maxContentHeightFromTallestSnippetContentHeight(tallestSnippetContentHeight: tallestSnippetContentHeight, tallestReferenceContentHeight: tallestReferenceChangeDetailHeight, traitCollection: traitCollection, theme: theme)
+
+ let finalHeight = maxContentHeight == 0 ? 0 : maxContentHeight + Self.sideScrollingCellPadding.top + Self.sideScrollingCellPadding.bottom + Self.additionalPointsForShadow
+ self.sideScrollingCollectionViewHeight = finalHeight
+ return finalHeight
+ }
+
+ func changeDetailsForTraitCollection(_ traitCollection: UITraitCollection, theme: Theme) -> [ChangeDetail] {
+ if let changeDetails = changeDetails {
+ return changeDetails
+ }
+
+ var changeDetails: [ChangeDetail] = []
+
+ switch typedEvent {
+ case .newTalkPageTopic(let newTalkPageTopic):
+ let attributedString = newTalkPageTopic.snippet.byAttributingHTML(with: Self.changeDetailDescriptionTextStyle, boldWeight: Self.changeDetailDescriptionFontWeight, matching: traitCollection, color: theme.colors.primaryText, linkColor: theme.colors.link, handlingLists: true, handlingSuperSubscripts: true)
+ let changeDetail = ChangeDetail.snippet(Snippet(description: attributedString))
+ changeDetails.append(changeDetail)
+ case .large(let largeChange):
+ for typedChange in largeChange.typedChanges {
+ switch typedChange {
+ case .addedText(let addedText):
+ // TODO: Add highlighting here. For snippetType 1, add a highlighting attribute across the whole string. Otherwise, seek out highlight-add span ranges and add those attributes
+ guard let snippet = addedText.snippet else {
+ continue
+ }
+
+ let attributedString = snippet.byAttributingHTML(with: Self.changeDetailDescriptionTextStyle, boldWeight: Self.changeDetailDescriptionFontWeight, matching: traitCollection, color: theme.colors.primaryText, handlingLinks: true, linkColor: theme.colors.link, handlingLists: true, handlingSuperSubscripts: true)
+ let changeDetail = ChangeDetail.snippet(Snippet(description: attributedString))
+ changeDetails.append(changeDetail)
+ case .deletedText:
+ continue
+ case .newTemplate(let newTemplate):
+ for template in newTemplate.typedTemplates {
+
+ switch template {
+ case .articleDescription(let articleDescription):
+ let font = UIFont.wmf_font(Self.changeDetailDescriptionTextStyle, compatibleWithTraitCollection: traitCollection)
+ let attributes = [NSAttributedString.Key.font: font,
+ NSAttributedString.Key.foregroundColor:
+ theme.colors.primaryText]
+ let attributedString = NSAttributedString(string: articleDescription.text, attributes: attributes)
+ let changeDetail = ChangeDetail.snippet(Snippet(description: attributedString))
+ changeDetails.append(changeDetail)
+ // tonitodo: these code blocks are all very similar. make a generic method instead?
+ case .bookCitation(let bookCitation):
+ let typeText = referenceTypeForTemplate(template, traitCollection: traitCollection, theme: theme)
+ let accessYear = accessDateYearForTemplate(template, traitCollection: traitCollection, theme: theme)
+ let bookCitationDescription = descriptionForBookCitation(bookCitation, traitCollection: traitCollection, theme: theme)
+ if let reference = Reference(type: typeText, description: bookCitationDescription, accessDateYearDisplay: accessYear) {
+ let changeDetail = ChangeDetail.reference(reference)
+ changeDetails.append(changeDetail)
+ }
+ case .journalCitation(let journalCitation):
+ let typeText = referenceTypeForTemplate(template, traitCollection: traitCollection, theme: theme)
+ let accessYear = accessDateYearForTemplate(template, traitCollection: traitCollection, theme: theme)
+ let citationDescription = descriptionForJournalCitation(journalCitation, traitCollection: traitCollection, theme: theme)
+ if let reference = Reference(type: typeText, description: citationDescription, accessDateYearDisplay: accessYear) {
+ let changeDetail = ChangeDetail.reference(reference)
+ changeDetails.append(changeDetail)
+ }
+ case .newsCitation(let newsCitation):
+ let typeText = referenceTypeForTemplate(template, traitCollection: traitCollection, theme: theme)
+ let accessYear = accessDateYearForTemplate(template, traitCollection: traitCollection, theme: theme)
+ let citationDescription = descriptionForNewsCitation(newsCitation, traitCollection: traitCollection, theme: theme)
+ if let reference = Reference(type: typeText, description: citationDescription, accessDateYearDisplay: accessYear) {
+ let changeDetail = ChangeDetail.reference(reference)
+ changeDetails.append(changeDetail)
+ }
+ case .websiteCitation(let websiteCitation):
+ let typeText = referenceTypeForTemplate(template, traitCollection: traitCollection, theme: theme)
+ let accessYear = accessDateYearForTemplate(template, traitCollection: traitCollection, theme: theme)
+ let citationDescription = descriptionForWebsiteCitation(websiteCitation, traitCollection: traitCollection, theme: theme)
+ if let reference = Reference(type: typeText, description: citationDescription, accessDateYearDisplay: accessYear) {
+ let changeDetail = ChangeDetail.reference(reference)
+ changeDetails.append(changeDetail)
+ }
+ }
+ }
+ }
+ }
+ case .vandalismRevert:
+ return []
+ case .small:
+ assertionFailure("This should not happen")
+ return []
+ }
+
+ self.changeDetails = changeDetails
+ return changeDetails
+ }
+
+ // Note: heightForThreeLineSnippet and heightForReferenceTitle methods are placeholder calculations when determining a side scrolling cell's content height.
+ // When there are no reference cells, we are capping off article content snippet cells at 3 lines. If there are reference cells, snippet cells are allowed to show lines to the full height of the tallest reference cell.
+ // Reference cells titles are only ever 1 line, so we are using placeholder text to calculate that rather than going up against actual view model title values, since the height will be the same regardless of the size of the title value
+ // heightForThreeLine and heightForReferenceTitle only ever needs to be calculated once per traitCollection's preferredContentSize, so we are optimizing in the similar way that ArticleAsLivingDocViewModel's various NSAttributedStrings are optimized, i.e. calculate once, then reset when the traitCollection changes via resetAttributedStringsIfNeededWithTraitCollection.
+ private static var heightForThreeLineSnippet: CGFloat?
+ private static func heightForThreeLineSnippetForTraitCollection(_ traitCollection: UITraitCollection, theme: Theme) -> CGFloat {
+
+ if let heightForThreeLineSnippet = heightForThreeLineSnippet {
+ return heightForThreeLineSnippet
+ }
+
+ let snippetFont = UIFont.wmf_font(Self.changeDetailDescriptionTextStyle, compatibleWithTraitCollection: traitCollection)
+ let snippetAttributes = [NSAttributedString.Key.font: snippetFont]
+ let threeLineSnippetText = """
+ 1
+ 2
+ 3
+ """
+ let threeLineSnippetAttString = NSAttributedString(string: threeLineSnippetText, attributes: snippetAttributes)
+
+ let finalHeight = ceil(threeLineSnippetAttString.boundingRect(with: CGSize(width: Self.availableSideScrollingCellWidth, height: CGFloat.infinity), options: [.usesLineFragmentOrigin], context: nil).height)
+ heightForThreeLineSnippet = finalHeight
+ return finalHeight
+ }
+
+ private static var heightForReferenceTitle: CGFloat?
+ private static func heightForReferenceTitleForTraitCollection(_ traitCollection: UITraitCollection, theme: Theme) -> CGFloat {
+
+ if let heightForReferenceTitle = heightForReferenceTitle {
+ return heightForReferenceTitle
+ }
+
+ let referenceTitleFont = UIFont.wmf_font(Self.changeDetailReferenceTitleStyle, compatibleWithTraitCollection: traitCollection)
+ let referenceTitleAttributes = [NSAttributedString.Key.font: referenceTitleFont]
+ let oneLineTitleText = "1"
+ let oneLineTitleAttString = NSAttributedString(string: oneLineTitleText, attributes: referenceTitleAttributes)
+ let finalHeight = ceil(oneLineTitleAttString.boundingRect(with: CGSize(width: Self.availableSideScrollingCellWidth, height: CGFloat.infinity), options: [.usesLineFragmentOrigin], context: nil).height)
+ heightForReferenceTitle = finalHeight
+ return finalHeight
+ }
+
+ private func calculateTallestSnippetContentHeightInChangeDetails(changeDetails: [ChangeDetail]) -> CGFloat {
+ var tallestSnippetChangeDetailHeight: CGFloat = 0
+
+ changeDetails.forEach { (changeDetail) in
+ switch changeDetail {
+ case .snippet(let snippet):
+ let snippetHeight = ceil(snippet.description.boundingRect(with: CGSize(width: Self.availableSideScrollingCellWidth, height: CGFloat.infinity), options: [.usesLineFragmentOrigin], context: nil).height)
+
+ if tallestSnippetChangeDetailHeight < snippetHeight {
+ tallestSnippetChangeDetailHeight = snippetHeight
+ }
+
+ case .reference:
+ break
+ }
+ }
+
+ return tallestSnippetChangeDetailHeight
+ }
+
+ private func calculateTallestReferenceContentHeightInChangeDetails(changeDetails: [ChangeDetail], traitCollection: UITraitCollection, theme: Theme) -> CGFloat {
+ var tallestReferenceChangeDetailHeight: CGFloat = 0
+
+ changeDetails.forEach { (changeDetail) in
+ switch changeDetail {
+ case .snippet:
+ break
+ case .reference(let reference):
+ let titleHeight = Self.heightForReferenceTitleForTraitCollection(traitCollection, theme: theme)
+ let descriptionHeight = ceil(reference.description.boundingRect(with: CGSize(width: Self.availableSideScrollingCellWidth, height: CGFloat.infinity), options: [.usesLineFragmentOrigin], context: nil).height)
+ let totalHeight = titleHeight + Self.changeDetailReferenceTitleDescriptionSpacing + descriptionHeight
+
+ if tallestReferenceChangeDetailHeight < totalHeight {
+ tallestReferenceChangeDetailHeight = totalHeight
+ }
+ }
+ }
+
+ return tallestReferenceChangeDetailHeight
+ }
+
+ private func maxContentHeightFromTallestSnippetContentHeight(tallestSnippetContentHeight: CGFloat, tallestReferenceContentHeight: CGFloat, traitCollection: UITraitCollection, theme: Theme) -> CGFloat {
+
+ guard tallestSnippetContentHeight > 0 else {
+ return tallestReferenceContentHeight
+ }
+
+ let threeLineSnippetHeight = Self.heightForThreeLineSnippetForTraitCollection(traitCollection, theme: theme)
+ if tallestReferenceContentHeight == 0 {
+ return min(tallestSnippetContentHeight, threeLineSnippetHeight)
+ } else {
+ let finalSnippetHeight = min(tallestSnippetContentHeight, threeLineSnippetHeight)
+ return max(tallestReferenceContentHeight, finalSnippetHeight)
+ }
+ }
+
+ private func referenceTypeForTemplate(_ template: SignificantEvents.Template, traitCollection: UITraitCollection, theme: Theme) -> String {
+
+ var typeString: String
+ switch template {
+ case .articleDescription:
+ assertionFailure("This should not happen")
+ return ""
+ case .bookCitation:
+ typeString = CommonStrings.newBookReferenceTitle
+ case .journalCitation:
+ typeString = CommonStrings.newJournalReferenceTitle
+ case .newsCitation:
+ typeString = CommonStrings.newNewsReferenceTitle
+ case .websiteCitation:
+ typeString = CommonStrings.newWebsiteReferenceTitle
+
+ }
+
+ return typeString
+ }
+
+ private func accessDateYearForTemplate(_ template: SignificantEvents.Template, traitCollection: UITraitCollection, theme: Theme) -> String? {
+
+ let accessDateString: String?
+ switch template {
+ case .newsCitation(let newsCitation):
+ accessDateString = newsCitation.accessDateString
+ case .websiteCitation(let websiteCitation):
+ accessDateString = websiteCitation.accessDateString
+ default:
+ return nil
+ }
+
+ if let accessDateString = accessDateString {
+ let dateFormatter = DateFormatter.wmf_monthNameDayOfMonthNumberYear()
+ if let date = dateFormatter?.date(from: accessDateString) {
+ let yearDateFormatter = DateFormatter.wmf_year()
+ let year = yearDateFormatter?.string(from: date)
+ return year
+ }
+ }
+
+ return nil
+
+ }
+
+ private func descriptionForJournalCitation(_ journalCitation: SignificantEvents.Citation.Journal, traitCollection: UITraitCollection, theme: Theme) -> NSAttributedString? {
+
+ let font = UIFont.wmf_font(Self.changeDetailDescriptionTextStyle, compatibleWithTraitCollection: traitCollection)
+ let boldFont = UIFont.wmf_font(Self.changeDetailDescriptionTextStyleItalic, compatibleWithTraitCollection: traitCollection)
+ let attributes = [NSAttributedString.Key.font: font,
+ NSAttributedString.Key.foregroundColor:
+ theme.colors.primaryText]
+ let boldAttributes = [NSAttributedString.Key.font: boldFont,
+ NSAttributedString.Key.foregroundColor:
+ theme.colors.primaryText]
+
+ let titleString = "\"\(journalCitation.title)\" "
+ let mutableAttributedString = mutableString(from: titleString, linkedTo: journalCitation.urlString, with: attributes, linkColor: theme.colors.link)
+ let titleAttributedString = mutableAttributedString.copy() as? NSAttributedString
+
+ var descriptionStart = ""
+ if let firstName = journalCitation.firstName {
+ if let lastName = journalCitation.lastName {
+ descriptionStart += "\(lastName), \(firstName)"
+ }
+ } else {
+ if let lastName = journalCitation.lastName {
+ descriptionStart += "\(lastName)"
+ }
+ }
+
+ if let sourceDate = journalCitation.sourceDateString {
+ descriptionStart += " (\(sourceDate)). "
+ } else {
+ descriptionStart += ". "
+ }
+
+ let descriptionStartAttributedString = NSAttributedString(string: descriptionStart, attributes: attributes)
+
+ var volumeString = ""
+ if let volumeNumber = journalCitation.volumeNumber {
+ volumeString = String.localizedStringWithFormat(CommonStrings.newJournalReferenceVolume, volumeNumber)
+ }
+ let volumeAttributedString = NSAttributedString(string: "\(volumeString) ", attributes: boldAttributes)
+
+ var descriptionEnd = ""
+ if let database = journalCitation.database {
+ let viaDatabaseString = String.localizedStringWithFormat(CommonStrings.newJournalReferenceDatabase, database)
+ if let pages = journalCitation.pages {
+ descriptionEnd += "\(pages) - \(viaDatabaseString)."
+ } else {
+ descriptionEnd += "\(viaDatabaseString)."
+ }
+ } else {
+ if let pages = journalCitation.pages {
+ descriptionEnd += "\(pages)."
+ }
+ }
+
+ let descriptionEndAttributedString = NSAttributedString(string: descriptionEnd, attributes: attributes)
+
+ let finalMutableAttributedString = NSMutableAttributedString(string: "")
+ finalMutableAttributedString.append(descriptionStartAttributedString)
+ if let titleAttributedString = titleAttributedString {
+ finalMutableAttributedString.append(titleAttributedString)
+ }
+ finalMutableAttributedString.append(volumeAttributedString)
+ finalMutableAttributedString.append(descriptionEndAttributedString)
+
+ return finalMutableAttributedString.copy() as? NSAttributedString
+
+ }
+
+ private func descriptionForWebsiteCitation(_ websiteCitation: SignificantEvents.Citation.Website, traitCollection: UITraitCollection, theme: Theme) -> NSAttributedString? {
+ let font = UIFont.wmf_font(Self.changeDetailDescriptionTextStyle, compatibleWithTraitCollection: traitCollection)
+ let italicFont = UIFont.wmf_font(Self.changeDetailDescriptionTextStyleItalic, compatibleWithTraitCollection: traitCollection)
+ let attributes = [NSAttributedString.Key.font: font,
+ NSAttributedString.Key.foregroundColor:
+ theme.colors.primaryText]
+ let italicAttributes = [NSAttributedString.Key.font: italicFont,
+ NSAttributedString.Key.foregroundColor:
+ theme.colors.primaryText]
+
+ let titleString = "\"\(websiteCitation.title)\" "
+ let mutableAttributedString = mutableString(from: titleString, linkedTo: websiteCitation.urlString, with: attributes, linkColor: theme.colors.link)
+ let titleAttributedString = mutableAttributedString.copy() as? NSAttributedString
+
+ var publisherText = ""
+ if let publisher = websiteCitation.publisher {
+ publisherText = "\(publisher). "
+ }
+
+ let publisherAttributedString = NSAttributedString(string: publisherText, attributes: italicAttributes)
+
+ var accessDateString = ""
+ if let accessDate = websiteCitation.accessDateString {
+ accessDateString = "\(accessDate). "
+ }
+
+ let accessDateAttributedString = NSAttributedString(string: accessDateString, attributes: attributes)
+
+ let finalMutableAttributedString = NSMutableAttributedString(string: "")
+ if let titleAttributedString = titleAttributedString {
+ finalMutableAttributedString.append(titleAttributedString)
+ }
+ finalMutableAttributedString.append(publisherAttributedString)
+ finalMutableAttributedString.append(accessDateAttributedString)
+
+ if let archiveDateString = websiteCitation.archiveDateString,
+ let archiveUrlString = websiteCitation.archiveDotOrgUrlString,
+ URL(string: archiveUrlString) != nil {
+ let archiveLinkText = CommonStrings.newWebsiteReferenceArchiveUrlText
+ let archiveLinkMutableAttributedString = mutableString(from: archiveLinkText, linkedTo: archiveUrlString, with: attributes, linkColor: theme.colors.link)
+
+ if let archiveLinkAttributedString = archiveLinkMutableAttributedString.copy() as? NSAttributedString {
+ let lastText = String.localizedStringWithFormat(CommonStrings.newWebsiteReferenceArchiveDateText, archiveDateString)
+ let lastAttributedString = NSAttributedString(string: " \(lastText)", attributes: attributes)
+
+ finalMutableAttributedString.append(archiveLinkAttributedString)
+ finalMutableAttributedString.append(lastAttributedString)
+
+ }
+
+ }
+
+ return finalMutableAttributedString.copy() as? NSAttributedString
+ }
+
+ private func descriptionForNewsCitation(_ newsCitation: SignificantEvents.Citation.News, traitCollection: UITraitCollection, theme: Theme) -> NSAttributedString? {
+
+ let font = UIFont.wmf_font(Self.changeDetailDescriptionTextStyle, compatibleWithTraitCollection: traitCollection)
+ let italicFont = UIFont.wmf_font(Self.changeDetailDescriptionTextStyleItalic, compatibleWithTraitCollection: traitCollection)
+ let attributes = [NSAttributedString.Key.font: font,
+ NSAttributedString.Key.foregroundColor:
+ theme.colors.primaryText]
+ let italicAttributes = [NSAttributedString.Key.font: italicFont,
+ NSAttributedString.Key.foregroundColor:
+ theme.colors.primaryText]
+
+ let titleString = "\"\(newsCitation.title)\" "
+ let mutableAttributedString = mutableString(from: titleString, linkedTo: newsCitation.urlString, with: attributes, linkColor: theme.colors.link)
+ let titleAttributedString = mutableAttributedString.copy() as? NSAttributedString
+
+ var descriptionStart = ""
+ if let firstName = newsCitation.firstName {
+ if let lastName = newsCitation.lastName {
+ descriptionStart += "\(lastName), \(firstName) "
+ }
+ } else {
+ if let lastName = newsCitation.lastName {
+ descriptionStart += "\(lastName) "
+ }
+ }
+
+ if let sourceDate = newsCitation.sourceDateString {
+ descriptionStart += "(\(sourceDate)). "
+ } else {
+ descriptionStart += ". "
+ }
+
+ let descriptionStartAttributedString = NSAttributedString(string: descriptionStart, attributes: attributes)
+
+ var publicationText = ""
+ if let publication = newsCitation.publication {
+ publicationText = "\(publication). "
+ }
+
+ let publicationAttributedString = NSAttributedString(string: publicationText, attributes: italicAttributes)
+
+ var retrievedString = ""
+ if let accessDate = newsCitation.accessDateString {
+ retrievedString = String.localizedStringWithFormat(CommonStrings.newNewsReferenceRetrievedDate, accessDate)
+ }
+
+ let retrievedDateAttributedString = NSAttributedString(string: "\(retrievedString) ", attributes: attributes)
+
+ let finalMutableAttributedString = NSMutableAttributedString(string: "")
+ finalMutableAttributedString.append(descriptionStartAttributedString)
+ if let titleAttributedString = titleAttributedString {
+ finalMutableAttributedString.append(titleAttributedString)
+ }
+ finalMutableAttributedString.append(publicationAttributedString)
+ finalMutableAttributedString.append(retrievedDateAttributedString)
+
+ return finalMutableAttributedString.copy() as? NSAttributedString
+
+ }
+
+ private func descriptionForBookCitation(_ bookCitation: SignificantEvents.Citation.Book, traitCollection: UITraitCollection, theme: Theme) -> NSAttributedString? {
+
+ let font = UIFont.wmf_font(Self.changeDetailDescriptionTextStyle, compatibleWithTraitCollection: traitCollection)
+ let italicFont = UIFont.wmf_font(Self.changeDetailDescriptionTextStyleItalic, compatibleWithTraitCollection: traitCollection)
+ let attributes = [NSAttributedString.Key.font: font,
+ NSAttributedString.Key.foregroundColor:
+ theme.colors.primaryText]
+ let italicAttributes = [NSAttributedString.Key.font: italicFont,
+ NSAttributedString.Key.foregroundColor:
+ theme.colors.primaryText]
+
+ let titleAttributedString = NSAttributedString(string: "\(bookCitation.title) ", attributes: italicAttributes)
+
+ var descriptionStart = ""
+ if let firstName = bookCitation.firstName {
+ if let lastName = bookCitation.lastName {
+ descriptionStart += "\(lastName), \(firstName)"
+ }
+ } else {
+ if let lastName = bookCitation.lastName {
+ descriptionStart += "\(lastName)"
+ }
+ }
+
+ if let yearOfPub = bookCitation.yearPublished {
+ descriptionStart += " (\(yearOfPub)). "
+ } else {
+ descriptionStart += ". "
+ }
+
+ let descriptionStartAttributedString = NSAttributedString(string: descriptionStart, attributes: attributes)
+
+ var descriptionMiddle = ""
+ if let locationPublished = bookCitation.locationPublished {
+ if let publisher = bookCitation.publisher {
+ descriptionMiddle += "\(locationPublished): \(publisher). "
+ } else {
+ descriptionMiddle += "\(locationPublished). "
+ }
+ } else {
+ if let publisher = bookCitation.publisher {
+ descriptionMiddle += "\(publisher). "
+ }
+ }
+
+ if let pagesCited = bookCitation.pagesCited {
+ descriptionMiddle += "pp. \(pagesCited) "
+ }
+
+ let descriptionMiddleAttributedString = NSAttributedString(string: descriptionMiddle, attributes: attributes)
+
+ var isbnAttributedString: NSAttributedString?
+ if let isbn = bookCitation.isbn {
+ let isbnPrefix = "ISBN: "
+ let mutableAttributedString = NSMutableAttributedString(string: "\(isbnPrefix + isbn)", attributes: attributes)
+ let isbnTitle = "Special:BookSources"
+ let isbnURL = Configuration.current.articleURLForHost(Configuration.Domain.englishWikipedia, languageVariantCode: nil, appending: [isbnTitle, isbn])
+ let range = NSRange(location: 0, length: isbnPrefix.count + isbn.count)
+ if let isbnURL = isbnURL {
+ mutableAttributedString.addAttributes([NSAttributedString.Key.link : isbnURL,
+ NSAttributedString.Key.foregroundColor: theme.colors.link], range: range)
+ } else {
+ mutableAttributedString.addAttributes(attributes, range: range)
+ }
+
+ isbnAttributedString = mutableAttributedString.copy() as? NSAttributedString
+ }
+
+ let finalMutableAttributedString = NSMutableAttributedString(string: "")
+ finalMutableAttributedString.append(descriptionStartAttributedString)
+ finalMutableAttributedString.append(titleAttributedString)
+ finalMutableAttributedString.append(descriptionMiddleAttributedString)
+ if let isbnAttributedString = isbnAttributedString {
+ finalMutableAttributedString.append(isbnAttributedString)
+ }
+
+ return finalMutableAttributedString.copy() as? NSAttributedString
+
+ }
+
+ private func mutableString(from text: String, linkedTo urlString: String?, with textAttributes: [NSAttributedString.Key:Any], linkColor: UIColor) -> NSMutableAttributedString {
+ let mutableAttributedString: NSMutableAttributedString
+ if let urlString = urlString, let url = URL(string: urlString), let externalLinkIcon = UIImage(named: "mini-external") {
+ mutableAttributedString = NSMutableAttributedString(string: text.trimmingCharacters(in: .whitespaces), attributes: textAttributes)
+ mutableAttributedString.append(NSAttributedString(string: " "))
+ let externalLinkString = NSAttributedString(attachment: NSTextAttachment(image: externalLinkIcon))
+ mutableAttributedString.append(externalLinkString)
+ mutableAttributedString.append(NSAttributedString(string: " "))
+ let range = NSRange(location: 0, length: mutableAttributedString.length)
+ mutableAttributedString.addAttributes([NSAttributedString.Key.link : url,
+ NSAttributedString.Key.foregroundColor: linkColor], range: range)
+ } else {
+ mutableAttributedString = NSMutableAttributedString(string: text, attributes: textAttributes)
+ let range = NSRange(location: 0, length: text.count)
+ mutableAttributedString.addAttributes(textAttributes, range: range)
+ }
+ return mutableAttributedString
+ }
+
+ private func getTimestampString() -> String? {
+ switch typedEvent {
+ case .newTalkPageTopic(let newTalkPageTopic):
+ return newTalkPageTopic.timestampString
+ case .large(let largeChange):
+ return largeChange.timestampString
+ case .vandalismRevert(let vandalismRevert):
+ return vandalismRevert.timestampString
+ case .small:
+ return nil
+ }
+ }
+
+ // Only used in the html portion of the feature
+ func fullyRelativeTimestampForDisplay() -> String? {
+
+ guard let timestampString = getTimestampString() else {
+ return nil
+ }
+
+ return ArticleAsLivingDocViewModel.displayTimestamp(timestampString: timestampString, fullyRelative: true)
+ }
+
+ func timestampForDisplay() -> String? {
+ if let displayTimestamp = displayTimestamp {
+ return displayTimestamp
+ } else if let timestampString = getTimestampString() {
+ self.displayTimestamp = ArticleAsLivingDocViewModel.eventDisplayTimestamp(timestampString: timestampString)
+ }
+
+ return displayTimestamp
+ }
+
+ private func userNameAndEditCount() -> (userName: String, editCount: UInt?)? {
+ let userName: String
+ let editCount: UInt?
+ switch typedEvent {
+ case .newTalkPageTopic(let newTalkPageTopic):
+ userName = newTalkPageTopic.user
+ editCount = newTalkPageTopic.userEditCount
+ case .large(let largeChange):
+ userName = largeChange.user
+ editCount = largeChange.userEditCount
+ case .vandalismRevert(let vandalismRevert):
+ userName = vandalismRevert.user
+ editCount = vandalismRevert.userEditCount
+ case .small:
+ return nil
+ }
+
+ return (userName: userName, editCount: editCount)
+ }
+
+ static var botIconName: String {
+ return "article-as-living-doc-svg-bot"
+ }
+
+ static var anonymousIconName: String = "article-as-living-doc-svg-anon"
+
+ private func userInfoHtmlSnippet() -> String? {
+ guard let userNameAndEditCount = self.userNameAndEditCount() else {
+ assertionFailure("Shouldn't reach this point")
+ return nil
+ }
+ let userName = userNameAndEditCount.userName
+ let editCount = userNameAndEditCount.editCount
+
+ if let editCount = editCount,
+ userType != .anonymous {
+ let formattedEditCount = NumberFormatter.localizedThousandsStringFromNumber(NSNumber(value: editCount)).localizedLowercase
+ let userInfo = String.localizedStringWithFormat(CommonStrings.revisionUserInfo, userName, formattedEditCount)
+
+ let rangeOfUserName = (userInfo as NSString).range(of: userName)
+ let rangeValid = rangeOfUserName.location != NSNotFound && rangeOfUserName.location + rangeOfUserName.length <= userInfo.count
+ let userNameHrefString = "#significant-events-username-\(userName)"
+ if rangeValid {
+
+ let mutableUserInfo = NSMutableString(string: userInfo)
+
+ let linkStartInsert: String
+ if userType == .bot {
+ linkStartInsert = "
"
+ } else {
+ linkStartInsert = ""
+ }
+ let linkEndInsert = ""
+ mutableUserInfo.insert(linkStartInsert, at: rangeOfUserName.location)
+ mutableUserInfo.insert(linkEndInsert, at: rangeOfUserName.location + rangeOfUserName.length + linkStartInsert.count)
+
+ if let userInfoResult = mutableUserInfo.copy() as? NSString {
+ return (userInfoResult as String)
+ } else {
+ assertionFailure("This shouldn't happen")
+ return nil
+ }
+ }
+ } else {
+ return "
\(CommonStrings.revisionUserInfoAnonymous)"
+
+ }
+
+ return nil
+ }
+
+ func userInfoForTraitCollection(_ traitCollection: UITraitCollection, theme: Theme) -> NSAttributedString? {
+ if let userInfo = userInfo {
+ return userInfo
+ }
+
+ guard let userNameAndEditCount = self.userNameAndEditCount() else {
+ assertionFailure("Shouldn't reach this point")
+ return nil
+ }
+
+ let userName = userNameAndEditCount.userName
+ let maybeEditCount = userNameAndEditCount.editCount
+
+ guard let editCount = maybeEditCount,
+ userType != .anonymous else {
+ let anonymousUserInfo = CommonStrings.revisionUserInfoAnonymous
+
+ let font = UIFont.wmf_font(.subheadline, compatibleWithTraitCollection: traitCollection)
+ let attributes = [NSAttributedString.Key.font: font,
+ NSAttributedString.Key.foregroundColor: theme.colors.secondaryText]
+ let mutableAttributedString = NSMutableAttributedString(string: anonymousUserInfo, attributes: attributes)
+ addIcon(to: mutableAttributedString, at: 0, for: userType)
+ // Need this next line to appropriately color the icon
+ mutableAttributedString.addAttributes(attributes, range: NSRange(location: 0, length: 2))
+ guard let attributedString = mutableAttributedString.copy() as? NSAttributedString else {
+ return nil
+ }
+
+ self.userInfo = attributedString
+ return attributedString
+ }
+
+ let formattedEditCount = NumberFormatter.localizedThousandsStringFromNumber(NSNumber(value: editCount)).localizedLowercase
+ let userInfo = String.localizedStringWithFormat( CommonStrings.revisionUserInfo, userName, formattedEditCount)
+
+ let font = UIFont.wmf_font(.subheadline, compatibleWithTraitCollection: traitCollection)
+ let attributes = [NSAttributedString.Key.font: font,
+ NSAttributedString.Key.foregroundColor: theme.colors.secondaryText]
+ let rangeOfUserName = (userInfo as NSString).range(of: userName)
+ let rangeValid = rangeOfUserName.location != NSNotFound && rangeOfUserName.location + rangeOfUserName.length <= userInfo.count
+
+ guard let title = "User:\(userName)".percentEncodedPageTitleForPathComponents,
+ let userNameURL = Configuration.current.articleURLForHost(Configuration.Domain.englishWikipedia, languageVariantCode: nil, appending: [title]),
+ rangeValid else {
+ let attributedString = NSAttributedString(string: userInfo, attributes: attributes)
+ self.userInfo = attributedString
+ return attributedString
+ }
+
+ let mutableAttributedString = NSMutableAttributedString(string: userInfo, attributes: attributes)
+ mutableAttributedString.addAttribute(NSAttributedString.Key.link, value: userNameURL as NSURL, range: rangeOfUserName)
+ mutableAttributedString.addAttribute(NSAttributedString.Key.foregroundColor, value: theme.colors.link, range: rangeOfUserName)
+
+ addIcon(to: mutableAttributedString, at: rangeOfUserName.location, for: userType)
+
+ guard let attributedString = mutableAttributedString.copy() as? NSAttributedString else {
+ return nil
+ }
+
+ self.userInfo = attributedString
+ return attributedString
+ }
+
+ func addIcon(to mutableAttributedString: NSMutableAttributedString, at location: Int, for userType: UserType) {
+ if userType == .bot || userType == .anonymous {
+ let imageAttachment = NSTextAttachment()
+ imageAttachment.image = UIImage(named: (userType == .bot ? Self.botIconName : Self.anonymousIconName))
+ let imageString = NSAttributedString(attachment: imageAttachment)
+ mutableAttributedString.insert(imageString, at: location)
+ mutableAttributedString.insert(NSAttributedString(string: " "), at: location + imageString.length)
+ }
+ }
+
+}
diff --git a/Apps/Wikipedia/WMF Framework/Significant Events Endpoint/SignificantEventsFetcher.swift b/Apps/Wikipedia/WMF Framework/Significant Events Endpoint/SignificantEventsFetcher.swift
new file mode 100644
index 0000000..011bb6f
--- /dev/null
+++ b/Apps/Wikipedia/WMF Framework/Significant Events Endpoint/SignificantEventsFetcher.swift
@@ -0,0 +1,119 @@
+import Foundation
+
+enum SignificantEventsFetcherError: Error {
+ case failureToGenerateURL
+ case missingSignificantEvents
+}
+
+public class SignificantEventsFetcher: Fetcher {
+
+ public func fetchSignificantEvents(rvStartId: UInt? = nil, title: String, siteURL: URL, completion: @escaping ((Result) -> Void)) {
+
+ guard let url = significantEventsURL(rvStartId: rvStartId, title: title, siteURL: siteURL) else {
+ completion(.failure(SignificantEventsFetcherError.failureToGenerateURL))
+ return
+ }
+
+ let request = URLRequest(url: url)
+
+ _ = session.jsonDecodableTask(with: request) { (significantEvents: SignificantEvents?, response, error) in
+ if let error = error {
+ completion(.failure(error))
+ return
+ }
+
+ if let statusCode = (response as? HTTPURLResponse)?.statusCode,
+ statusCode != 200 {
+ completion(.failure(RequestError.unexpectedResponse))
+ return
+ }
+
+ guard let significantEvents = significantEvents else {
+ completion(.failure(SignificantEventsFetcherError.missingSignificantEvents))
+ return
+ }
+
+ completion(.success(significantEvents))
+ }
+
+ }
+
+ private func significantEventsURL(rvStartId: UInt? = nil, title: String, siteURL: URL) -> URL? {
+ let labsHost = "mobileapps-ios-experiments.wmflabs.org"
+ guard let siteHost = siteURL.host,
+ let percentEncodedTitle = title.percentEncodedPageTitleForPathComponents else {
+ return nil
+ }
+
+ let pathComponents = [siteHost, "v1", "page", "significant-events", percentEncodedTitle]
+ var components = URLComponents()
+ components.host = labsHost
+ components.scheme = "https"
+ components.replacePercentEncodedPathWithPathComponents(pathComponents)
+ if let rvStartId = rvStartId {
+ let queryParameters = ["rvstartid": rvStartId]
+ components.replacePercentEncodedQueryWithQueryParameters(queryParameters)
+ }
+
+ return components.url
+ }
+
+ private struct EditMetrics: Decodable {
+ let items: [Item]?
+
+ struct Item: Decodable {
+ let results: [Result]?
+
+ struct Result: Decodable {
+ let edits: Int?
+ }
+ }
+ }
+
+ public func fetchEditMetrics(for pageTitle: String, pageURL: URL, completion: @escaping (Result<[NSNumber], Error>) -> Void ) {
+ DispatchQueue.global(qos: .userInitiated).async {
+ guard
+ let title = pageTitle.percentEncodedPageTitleForPathComponents,
+ let project = pageURL.wmf_site?.host,
+ let daysAgo = Calendar.current.date(byAdding: .day, value: -30, to: Date()),
+ let from = DateFormatter.wmf_englishUTCNonDelimitedYearMonthDay()?.string(from: daysAgo),
+ let to = DateFormatter.wmf_englishUTCNonDelimitedYearMonthDay()?.string(from: Date())
+ else {
+ completion(.failure(RequestError.invalidParameters))
+ return
+ }
+ let pathComponents = ["edits", "per-page", project, title, "all-editor-types", "daily", from, to]
+ let components = self.configuration.metricsAPIURLComponents(appending: pathComponents)
+ guard let url = components.url else {
+ completion(.failure(RequestError.invalidParameters))
+ return
+ }
+ self.session.jsonDecodableTask(with: url) { (editMetrics: EditMetrics?, response: URLResponse?, error: Error?) in
+ if let error = error {
+ completion(.failure(error))
+ return
+ }
+ guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
+ completion(.failure(RequestError.unexpectedResponse))
+ return
+ }
+ var allEdits = [NSNumber]()
+ guard
+ let items = editMetrics?.items,
+ let firstItem = items.first,
+ let results = firstItem.results
+ else {
+ completion(.failure(RequestError.noNewData))
+ return
+ }
+ for case let result in results {
+ guard let edits = result.edits else {
+ continue
+ }
+ allEdits.append(NSNumber(value: edits))
+ }
+ completion(.success(allEdits))
+ }
+ }
+ }
+}
diff --git a/Apps/Wikipedia/WMF Framework/Significant Events Endpoint/SignificantEventsModels.swift b/Apps/Wikipedia/WMF Framework/Significant Events Endpoint/SignificantEventsModels.swift
new file mode 100644
index 0000000..6d47e4c
--- /dev/null
+++ b/Apps/Wikipedia/WMF Framework/Significant Events Endpoint/SignificantEventsModels.swift
@@ -0,0 +1,608 @@
+import Foundation
+enum SignificantEventsDecodeError: Error {
+ case unableToParseIntoTypedEvents
+}
+
+public struct SignificantEvents: Decodable {
+ public let nextRvStartId: UInt?
+ public let sha: String?
+ private let untypedEvents: [UntypedEvent]
+ public let typedEvents: [TypedEvent]
+ public let summary: Summary
+
+ enum CodingKeys: String, CodingKey {
+ case nextRvStartId
+ case sha
+ case untypedEvents = "timeline"
+ case typedEvents
+ case summary
+ }
+
+ public struct Summary: Decodable {
+ public let earliestTimestampString: String
+ public let numChanges: UInt
+ public let numUsers: UInt
+
+ enum CodingKeys: String, CodingKey {
+ case earliestTimestampString = "earliestTimestamp"
+ case numChanges
+ case numUsers
+ }
+ }
+
+ public init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: CodingKeys.self)
+ nextRvStartId = try? container.decode(UInt.self, forKey: .nextRvStartId)
+ sha = try? container.decode(String.self, forKey: .sha)
+ summary = try container.decode(Summary.self, forKey: .summary)
+ let untypedEvents = try container.decode([UntypedEvent].self, forKey: .untypedEvents)
+
+ var typedEvents: [TypedEvent] = []
+
+ for untypedEvent in untypedEvents {
+ switch untypedEvent.outputType {
+ case .small:
+ if let event = Event.Small(untypedEvent: untypedEvent) {
+ typedEvents.append(.small(event))
+ }
+ case .large:
+ if let event = Event.Large(untypedEvent: untypedEvent) {
+ typedEvents.append(.large(event))
+ }
+ case .vandalismRevert:
+ if let event = Event.VandalismRevert(untypedEvent: untypedEvent) {
+ typedEvents.append(.vandalismRevert(event))
+ }
+ case .newTalkPageTopic:
+ if let event = Event.NewTalkPageTopic(untypedEvent: untypedEvent) {
+ typedEvents.append(.newTalkPageTopic(event))
+ }
+ }
+ }
+
+ // zero untyped events is a valid case if the user has paged to the end of the endpoint cache
+ // unTypedEvents > 0 and typedEvents == 0 is invalid, meaning all events failed to convert
+ guard typedEvents.count > 0 || untypedEvents.count == 0 else {
+ throw SignificantEventsDecodeError.unableToParseIntoTypedEvents
+ }
+
+ self.typedEvents = typedEvents
+ self.untypedEvents = untypedEvents
+ }
+
+ public enum SnippetType: Int, Decodable {
+ case addedLine = 1
+ case addedAndDeletedInLine = 3
+ case addedAndDeletedInMovedLine = 5
+ }
+
+ public enum EventOutputType: String, Decodable {
+ case large = "large-change"
+ case small = "small-change"
+ case newTalkPageTopic = "new-talk-page-topic"
+ case vandalismRevert = "vandalism-revert"
+ }
+
+ public enum ChangeOutputType: String, Decodable {
+ case addedText = "added-text"
+ case deletedText = "deleted-text"
+ case newTemplate = "new-template"
+ }
+
+ public enum TypedEvent {
+ case large(Event.Large)
+ case small(Event.Small)
+ case vandalismRevert(Event.VandalismRevert)
+ case newTalkPageTopic(Event.NewTalkPageTopic)
+ }
+
+ public enum TypedChange {
+ case addedText(Change.AddedText)
+ case deletedText(Change.DeletedText)
+ case newTemplate(Change.NewTemplates)
+ }
+}
+
+// MARK: Events
+
+public extension SignificantEvents {
+
+ struct Event {
+
+ public struct Large {
+ let outputType: EventOutputType
+ public let revId: UInt
+ public let parentId: UInt
+ public let timestampString: String
+ public let user: String
+ public let userId: UInt
+ public let userGroups: [String]?
+ public let userEditCount: UInt?
+ public let typedChanges: [TypedChange]
+
+ init?(untypedEvent: UntypedEvent) {
+ guard let revId = untypedEvent.revId,
+ let parentId = untypedEvent.parentId,
+ let timestampString = untypedEvent.timestampString,
+ let user = untypedEvent.user,
+ let userId = untypedEvent.userId,
+ let untypedChanges = untypedEvent.untypedChanges else {
+ return nil
+ }
+
+ self.outputType = untypedEvent.outputType
+ self.revId = revId
+ self.parentId = parentId
+ self.timestampString = timestampString
+ self.user = user
+ self.userId = userId
+ self.userGroups = untypedEvent.userGroups
+ self.userEditCount = untypedEvent.userEditCount
+
+ var changes: [TypedChange] = []
+
+ for untypedChange in untypedChanges {
+ switch untypedChange.outputType {
+ case .addedText:
+ if let change = Change.AddedText(untypedChange: untypedChange) {
+ changes.append(.addedText(change))
+ }
+ case .deletedText:
+ if let change = Change.DeletedText(untypedChange: untypedChange) {
+ changes.append(.deletedText(change))
+ }
+ case .newTemplate:
+ if let change = Change.NewTemplates(untypedChange: untypedChange) {
+ changes.append(.newTemplate(change))
+ }
+ }
+ }
+
+ guard changes.count == untypedChanges.count else {
+ return nil
+ }
+
+ self.typedChanges = changes
+ }
+ }
+
+ public struct Small: Equatable {
+ let outputType: EventOutputType
+ public let revId: UInt
+ public let parentId: UInt
+ public let timestampString: String
+
+ fileprivate init?(untypedEvent: UntypedEvent) {
+ guard let revId = untypedEvent.revId,
+ let parentId = untypedEvent.parentId,
+ let timestampString = untypedEvent.timestampString else {
+ return nil
+ }
+
+ self.outputType = untypedEvent.outputType
+ self.revId = revId
+ self.parentId = parentId
+ self.timestampString = timestampString
+ }
+
+ public static func == (lhs: SignificantEvents.Event.Small, rhs: SignificantEvents.Event.Small) -> Bool {
+ return lhs.revId == rhs.revId
+ }
+ }
+
+ public struct VandalismRevert {
+ let outputType: EventOutputType
+ public let revId: UInt
+ public let parentId: UInt
+ public let timestampString: String
+ public let user: String
+ public let userId: UInt
+ public let sections: [String]
+ public let userGroups: [String]?
+ public let userEditCount: UInt?
+
+ fileprivate init?(untypedEvent: UntypedEvent) {
+ guard let revId = untypedEvent.revId,
+ let parentId = untypedEvent.parentId,
+ let timestampString = untypedEvent.timestampString,
+ let user = untypedEvent.user,
+ let userId = untypedEvent.userId,
+ let sections = untypedEvent.sections else {
+ return nil
+ }
+
+ self.outputType = untypedEvent.outputType
+ self.revId = revId
+ self.parentId = parentId
+ self.timestampString = timestampString
+ self.user = user
+ self.userId = userId
+ self.sections = sections
+ self.userGroups = untypedEvent.userGroups
+ self.userEditCount = untypedEvent.userEditCount
+ }
+ }
+
+ public struct NewTalkPageTopic {
+ let outputType: EventOutputType
+ let revId: UInt
+ let parentId: UInt
+ public let timestampString: String
+ public let user: String
+ public let userId: UInt
+ public let section: String?
+ public let snippet: String
+ public let userGroups: [String]?
+ public let userEditCount: UInt?
+
+ fileprivate init?(untypedEvent: UntypedEvent) {
+ guard let revId = untypedEvent.revId,
+ let parentId = untypedEvent.parentId,
+ let timestampString = untypedEvent.timestampString,
+ let user = untypedEvent.user,
+ let userId = untypedEvent.userId,
+ let snippet = untypedEvent.snippet else {
+ return nil
+ }
+
+ self.outputType = untypedEvent.outputType
+ self.revId = revId
+ self.parentId = parentId
+ self.timestampString = timestampString
+ self.user = user
+ self.userId = userId
+ self.section = untypedEvent.section
+ self.snippet = snippet
+ self.userGroups = untypedEvent.userGroups
+ self.userEditCount = untypedEvent.userEditCount
+ }
+ }
+ }
+}
+
+// MARK: Changes
+
+public extension SignificantEvents {
+
+ struct Change {
+
+ public struct AddedText {
+ let outputType: ChangeOutputType
+ public let sections: [String]
+ public let snippet: String?
+ public let snippetType: SnippetType
+ public let characterCount: UInt
+
+ fileprivate init?(untypedChange: UntypedChange) {
+ guard let snippetType = untypedChange.snippetType,
+ let characterCount = untypedChange.characterCount else {
+ return nil
+ }
+
+ self.outputType = untypedChange.outputType
+ self.sections = untypedChange.sections
+ self.snippet = untypedChange.snippet
+ self.snippetType = snippetType
+ self.characterCount = characterCount
+ }
+ }
+
+ public struct DeletedText {
+ let outputType: ChangeOutputType
+ public let sections: [String]
+ public let characterCount: UInt
+
+ fileprivate init?(untypedChange: UntypedChange) {
+ guard let characterCount = untypedChange.characterCount else {
+ return nil
+ }
+
+ self.outputType = untypedChange.outputType
+ self.sections = untypedChange.sections
+ self.characterCount = characterCount
+ }
+ }
+
+ public struct NewTemplates {
+ let outputType: ChangeOutputType
+ public let sections: [String]
+ private let untypedTemplates: [[String: String]]
+ public let typedTemplates: [Template]
+
+ fileprivate init?(untypedChange: UntypedChange) {
+ guard let untypedTemplates = untypedChange.untypedTemplates else {
+ return nil
+ }
+
+ var typedTemplates: [Template] = []
+ self.outputType = untypedChange.outputType
+ self.sections = untypedChange.sections
+ self.untypedTemplates = untypedTemplates
+
+ for untypedTemplate in untypedTemplates {
+ guard let name = untypedTemplate["name"] else {
+ continue
+ }
+ if name.localizedCaseInsensitiveContains("cite") {
+ if name.localizedCaseInsensitiveContains("book"), let bookCitation = Citation.Book(dict: untypedTemplate) {
+ typedTemplates.append(.bookCitation(bookCitation))
+ } else if name.localizedCaseInsensitiveContains("journal"), let journalCitation = Citation.Journal(dict: untypedTemplate) {
+ typedTemplates.append(.journalCitation(journalCitation))
+ } else if name.localizedCaseInsensitiveContains("web"), let webCitation = Citation.Website(dict: untypedTemplate) {
+ typedTemplates.append(.websiteCitation(webCitation))
+ } else if name.localizedCaseInsensitiveContains("news"), let newsCitation = Citation.News(dict: untypedTemplate) {
+ typedTemplates.append(.newsCitation(newsCitation))
+ }
+ } else if name.localizedCaseInsensitiveContains("short description"), let articleDescription = ArticleDescription(dict: untypedTemplate) {
+ typedTemplates.append(.articleDescription(articleDescription))
+ }
+ }
+
+ self.typedTemplates = typedTemplates
+ }
+ }
+ }
+}
+
+// MARK: Templates
+
+public extension SignificantEvents {
+
+ enum Template {
+ case bookCitation(Citation.Book)
+ case articleDescription(ArticleDescription)
+ case journalCitation(Citation.Journal)
+ case newsCitation(Citation.News)
+ case websiteCitation(Citation.Website)
+ }
+
+ struct Citation {
+
+ // https://en.wikipedia.org/wiki/Template:Cite_book/TemplateData
+ public struct Book {
+ public let title: String
+ public let lastName: String?
+ public let firstName: String?
+ public let yearPublished: String?
+ public let locationPublished: String?
+ public let publisher: String?
+ public let pagesCited: String?
+ public let isbn: String?
+
+ init?(dict: [String: String]) {
+ guard let title = dict.nonEmptyValueForKey(key: "title") else {
+ return nil
+ }
+
+ self.title = title
+
+ let batch1 = dict.nonEmptyValueForKey(key: "last") ??
+ dict.nonEmptyValueForKey(key: "last1") ??
+ dict.nonEmptyValueForKey(key: "author") ??
+ dict.nonEmptyValueForKey(key: "author1") ??
+ dict.nonEmptyValueForKey(key: "author1-last")
+ let batch2 = dict.nonEmptyValueForKey(key: "author-last") ??
+ dict.nonEmptyValueForKey(key: "surname1") ??
+ dict.nonEmptyValueForKey(key: "author-last1") ??
+ dict.nonEmptyValueForKey(key: "subject1") ??
+ dict.nonEmptyValueForKey(key: "surname")
+ let batch3 = dict.nonEmptyValueForKey(key: "author-last") ??
+ dict.nonEmptyValueForKey(key: "subject")
+
+ self.lastName = batch1 ?? batch2 ?? batch3
+
+ self.firstName = dict.nonEmptyValueForKey(key: "first") ??
+ dict.nonEmptyValueForKey(key: "given") ??
+ dict.nonEmptyValueForKey(key: "author-first") ??
+ dict.nonEmptyValueForKey(key: "first1") ??
+ dict.nonEmptyValueForKey(key: "given1") ??
+ dict.nonEmptyValueForKey(key: "author-first1") ??
+ dict.nonEmptyValueForKey(key: "author1-first")
+
+ self.yearPublished = dict.nonEmptyValueForKey(key: "year")
+ self.locationPublished = dict.nonEmptyValueForKey(key: "location") ??
+ dict.nonEmptyValueForKey(key: "place")
+
+ self.publisher = dict.nonEmptyValueForKey(key: "publisher") ??
+ dict.nonEmptyValueForKey(key: "distributor") ??
+ dict.nonEmptyValueForKey(key: "institution") ??
+ dict.nonEmptyValueForKey(key: "newsgroup")
+
+ self.pagesCited = dict.nonEmptyValueForKey(key: "pages") ??
+ dict.nonEmptyValueForKey(key: "pp")
+
+ self.isbn = dict.nonEmptyValueForKey(key: "isbn", caseInsensitive: true) ??
+ dict.nonEmptyValueForKey(key: "isbn13", caseInsensitive: true)
+ }
+ }
+
+ // https://en.wikipedia.org/wiki/Template:Cite_journal#TemplateData
+ public struct Journal {
+ public let lastName: String?
+ public let firstName: String?
+ public let sourceDateString: String?
+ public let title: String
+ public let journal: String
+ public let urlString: String?
+ public let volumeNumber: String?
+ public let pages: String?
+ public let database: String?
+
+ init?(dict: [String: String]) {
+ guard let title = dict.nonEmptyValueForKey(key: "title"),
+ let journal = dict.nonEmptyValueForKey(key: "journal") else {
+ return nil
+ }
+
+ self.title = title
+ self.journal = journal
+
+ self.lastName = dict.nonEmptyValueForKey(key: "last") ??
+ dict.nonEmptyValueForKey(key: "author") ??
+ dict.nonEmptyValueForKey(key: "author1") ??
+ dict.nonEmptyValueForKey(key: "authors") ??
+ dict.nonEmptyValueForKey(key: "last1")
+
+ self.firstName = dict.nonEmptyValueForKey(key: "first") ??
+ dict.nonEmptyValueForKey(key: "first1")
+
+ self.sourceDateString = dict.nonEmptyValueForKey(key: "date")
+ self.urlString = dict.nonEmptyValueForKey(key: "url", caseInsensitive: true)
+ self.volumeNumber = dict.nonEmptyValueForKey(key: "volume")
+ self.pages = dict.nonEmptyValueForKey(key: "pages")
+ self.database = dict.nonEmptyValueForKey(key: "via")
+ }
+ }
+
+ // https://en.wikipedia.org/wiki/Template:Cite_news#TemplateData
+ public struct News {
+ public let lastName: String?
+ public let firstName: String?
+ public let sourceDateString: String?
+ public let title: String
+ public let urlString: String?
+ public let publication: String?
+ public let accessDateString: String?
+
+ init?(dict: [String: String]) {
+ guard let title = dict.nonEmptyValueForKey(key: "title") else {
+ return nil
+ }
+
+ self.title = title
+ self.lastName = dict.nonEmptyValueForKey(key: "last") ??
+ dict.nonEmptyValueForKey(key: "last1") ??
+ dict.nonEmptyValueForKey(key: "author") ??
+ dict.nonEmptyValueForKey(key: "author1") ??
+ dict.nonEmptyValueForKey(key: "authors")
+
+ self.firstName = dict.nonEmptyValueForKey(key: "first") ??
+ dict.nonEmptyValueForKey(key: "first1")
+
+ self.sourceDateString = dict.nonEmptyValueForKey(key: "date")
+ self.publication = dict.nonEmptyValueForKey(key: "work") ??
+ dict.nonEmptyValueForKey(key: "journal") ??
+ dict.nonEmptyValueForKey(key: "magazine") ??
+ dict.nonEmptyValueForKey(key: "periodical") ??
+ dict.nonEmptyValueForKey(key: "newspaper") ??
+ dict.nonEmptyValueForKey(key: "website")
+
+ self.urlString = dict.nonEmptyValueForKey(key: "url", caseInsensitive: true)
+ self.accessDateString = dict.nonEmptyValueForKey(key: "access-date") ?? dict.nonEmptyValueForKey(key: "accessdate")
+ }
+ }
+
+ // https://en.wikipedia.org/wiki/Template:Cite_web#TemplateData
+ public struct Website {
+
+ public let urlString: String
+ public let title: String
+ public let publisher: String?
+ public let accessDateString: String?
+ public let archiveDateString: String?
+ public let archiveDotOrgUrlString: String?
+
+ init?(dict: [String: String]) {
+ guard let title = dict.nonEmptyValueForKey(key: "title"),
+ let urlString = dict.nonEmptyValueForKey(key: "url", caseInsensitive: true) else {
+ return nil
+ }
+
+ self.title = title
+ self.urlString = urlString
+
+ self.publisher = dict.nonEmptyValueForKey(key: "publisher") ??
+ dict.nonEmptyValueForKey(key: "website") ??
+ dict.nonEmptyValueForKey(key: "work")
+
+ self.accessDateString = dict.nonEmptyValueForKey(key: "access-date") ?? dict.nonEmptyValueForKey(key: "accessdate")
+ self.archiveDateString = dict.nonEmptyValueForKey(key: "archive-date") ?? dict.nonEmptyValueForKey(key: "archivedate")
+ self.archiveDotOrgUrlString = dict.nonEmptyValueForKey(key: "archive-url") ?? dict.nonEmptyValueForKey(key: "archiveurl")
+ }
+ }
+ }
+
+ struct ArticleDescription {
+ public let text: String
+
+ init?(dict: [String: String]) {
+ guard let text = dict.nonEmptyValueForKey(key: "1") else {
+ return nil
+ }
+
+ self.text = text
+ }
+ }
+
+
+}
+
+// MARK: Untyped
+
+public extension SignificantEvents {
+ struct UntypedEvent: Decodable {
+ let outputType: EventOutputType
+ let revId: UInt?
+ let parentId: UInt?
+ let timestampString: String?
+ let user: String?
+ let userId: UInt?
+ let userGroups: [String]?
+ let userEditCount: UInt?
+ let count: UInt?
+ let sections: [String]?
+ let section: String?
+ let snippet: String?
+ let untypedChanges: [UntypedChange]?
+
+ enum CodingKeys: String, CodingKey {
+ case revId = "revid"
+ case parentId = "parentid"
+ case timestampString = "timestamp"
+ case outputType
+ case user
+ case userId = "userid"
+ case userGroups
+ case userEditCount
+ case count
+ case sections
+ case section
+ case snippet
+ case untypedChanges = "significantChanges"
+ }
+ }
+
+ struct UntypedChange: Decodable {
+ let outputType: ChangeOutputType
+ let sections: [String]
+ let snippet: String?
+ let snippetType: SnippetType?
+ let characterCount: UInt?
+ let untypedTemplates: [[String: String]]?
+
+ enum CodingKeys: String, CodingKey {
+ case outputType
+ case sections
+ case snippet
+ case snippetType
+ case characterCount
+ case untypedTemplates = "templates"
+ }
+ }
+}
+
+private extension Dictionary where Key == String, Value == String {
+ func nonEmptyValueForKey(key: String, caseInsensitive: Bool = false) -> String? {
+ guard let key = caseInsensitive
+ ? keys.first(where: {$0.caseInsensitiveCompare(key) == .orderedSame})
+ : key else {
+ return nil
+ }
+
+ if let value = self[key], !value.isEmpty {
+ return value
+ }
+
+ return nil
+ }
+}
diff --git a/Apps/Wikipedia/WMF Framework/String+Domains.swift b/Apps/Wikipedia/WMF Framework/String+Domains.swift
new file mode 100644
index 0000000..65264bc
--- /dev/null
+++ b/Apps/Wikipedia/WMF Framework/String+Domains.swift
@@ -0,0 +1,11 @@
+import Foundation
+
+extension String {
+ var withDotPrefix: String {
+ return "." + self
+ }
+
+ public func isDomainOrSubDomainOf(_ domain: String) -> Bool {
+ return self == domain || self.hasSuffix(domain.withDotPrefix)
+ }
+}
diff --git a/Apps/Wikipedia/WMF Framework/String+HTML.swift b/Apps/Wikipedia/WMF Framework/String+HTML.swift
new file mode 100644
index 0000000..0dd5b7d
--- /dev/null
+++ b/Apps/Wikipedia/WMF Framework/String+HTML.swift
@@ -0,0 +1,30 @@
+import Foundation
+
+extension String {
+ /// Converts HTML string to NSAttributedString by handling a limited subset of tags. Optionally bolds an additional string based on matching.
+ ///
+ /// This is used instead of alloc/init'ing the attributed string with @{NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType} because that approach proved to be slower and could't be called from a background thread. More info: https://developer.apple.com/documentation/foundation/nsattributedstring/1524613-initwithdata
+ ///
+ /// - Parameter textStyle: DynamicTextStyle to use with the resulting string
+ /// - Parameter boldWeight: Font weight for bolded parts of the string
+ /// - Parameter traitCollection: trait collection for font selection
+ /// - Parameter color: Text color
+ /// - Parameter handlingLinks: Whether or not link tags should be parsed and turned into links in the resulting string
+ /// - Parameter linkColor: Link text color
+ /// - Parameter handlingLists: Whether or not list tags should be parsed and styled in the resulting string
+ /// - Parameter handlingSuperSubscripts: whether or not super and subscript tags should be parsed and styled in the resulting string
+ /// - Parameter tagMapping: Lowercase string tag name to another lowercase string tag name - converts tags, for example, @{@"a":@"b"} will turn tags to tags
+ /// - Parameter additionalTagAttributes: Additional text attributes for given tags - lowercase tag name to attribute key/value pairs
+ /// - Returns: the resulting NSMutableAttributedString with styles applied to match the limited set of HTML tags that were parsed
+ public func byAttributingHTML(with textStyle: DynamicTextStyle, boldWeight: UIFont.Weight = .semibold, matching traitCollection: UITraitCollection, color: UIColor? = nil, handlingLinks: Bool = true, linkColor: UIColor? = nil, handlingLists: Bool = false, handlingSuperSubscripts: Bool = false, tagMapping: [String: String]? = nil, additionalTagAttributes: [String: [NSAttributedString.Key: Any]]? = nil) -> NSMutableAttributedString {
+ let font = UIFont.wmf_font(textStyle, compatibleWithTraitCollection: traitCollection)
+ let boldFont = UIFont.wmf_font(textStyle.with(weight: boldWeight), compatibleWithTraitCollection: traitCollection)
+ let italicFont = UIFont.wmf_font(textStyle.with(traits: [.traitItalic]), compatibleWithTraitCollection: traitCollection)
+ let boldItalicFont = UIFont.wmf_font(textStyle.with(weight: boldWeight, traits: [.traitItalic]), compatibleWithTraitCollection: traitCollection)
+ return (self as NSString).wmf_attributedStringFromHTML(with: font, boldFont: boldFont, italicFont: italicFont, boldItalicFont: boldItalicFont, color: color, linkColor: linkColor, handlingLinks: handlingLinks, handlingLists: handlingLists, handlingSuperSubscripts: handlingSuperSubscripts, tagMapping: tagMapping, additionalTagAttributes: additionalTagAttributes)
+ }
+
+ public var removingHTML: String {
+ return (self as NSString).wmf_stringByRemovingHTML()
+ }
+}
diff --git a/Apps/Wikipedia/WMF Framework/String+JavaScript.swift b/Apps/Wikipedia/WMF Framework/String+JavaScript.swift
new file mode 100644
index 0000000..38dbc2f
--- /dev/null
+++ b/Apps/Wikipedia/WMF Framework/String+JavaScript.swift
@@ -0,0 +1,11 @@
+import Foundation
+
+public extension String {
+ /// Can use ES6 backticks ` now instead of apostrophes.
+ /// Doing so means we *only* have to escape backticks instead of apostrophes, quotes and line breaks.
+ /// (May consider switching other native-to-JS messaging to do same later.)
+ var sanitizedForJavaScriptTemplateLiterals: String {
+ return replacingOccurrences(of: "([\\\\{}\\`])", with: "\\\\$1", options: [.regularExpression])
+ }
+}
+
diff --git a/Apps/Wikipedia/WMF Framework/String+LinkParsing.swift b/Apps/Wikipedia/WMF Framework/String+LinkParsing.swift
new file mode 100644
index 0000000..5dbd77e
--- /dev/null
+++ b/Apps/Wikipedia/WMF Framework/String+LinkParsing.swift
@@ -0,0 +1,136 @@
+/// Detect Wiki namespace in strings. For example, detect that "/wiki/Talk:Dog" is a talk page and "/wiki/Special:ApiSandbox" is a special page
+extension String {
+ static let namespaceRegex = try! NSRegularExpression(pattern: "^(.+?)_*:_*(.*)$")
+ // Assumes the input is the remainder of a /wiki/ path
+ func namespaceOfWikiResourcePath(with languageCode: String) -> PageNamespace {
+ guard let namespaceString = String.namespaceRegex.firstReplacementString(in: self) else {
+ return .main
+ }
+ return WikipediaURLTranslations.commonNamespace(for: namespaceString, in: languageCode) ?? .main
+ }
+
+ public func namespaceAndTitleOfWikiResourcePath(with languageCode: String) -> (namespace: PageNamespace, title: String) {
+ guard let result = String.namespaceRegex.firstMatch(in: self) else {
+ return (.main, self)
+ }
+ let namespaceString = String.namespaceRegex.replacementString(for: result, in: self, offset: 0, template: "$1")
+ guard let namespace = WikipediaURLTranslations.commonNamespace(for: namespaceString, in: languageCode) else {
+ return (.main, self)
+ }
+ let title = String.namespaceRegex.replacementString(for: result, in: self, offset: 0, template: "$2")
+ return (namespace, title)
+ }
+
+ static let wikiResourceRegex = try! NSRegularExpression(pattern: "^/wiki/(.+)$", options: .caseInsensitive)
+ var wikiResourcePath: String? {
+ return String.wikiResourceRegex.firstReplacementString(in: self)
+ }
+
+ static let wResourceRegex = try! NSRegularExpression(pattern: "^/w/(.+)$", options: .caseInsensitive)
+ public var wResourcePath: String? {
+ return String.wResourceRegex.firstReplacementString(in: self)
+ }
+
+ public var fullRange: NSRange {
+ return NSRange(startIndex.. String? {
+ if let namespaceString = String.namespaceRegex.firstReplacementString(in: self) {
+ let namespaceStringWithColon = "\(namespaceString):"
+ if namespaceOfWikiResourcePath(with: languageCode) == .talk {
+ return replacingOccurrences(of: namespaceStringWithColon, with: "")
+ }
+ }
+ return nil
+ }
+
+}
+
+/// Page title transformation
+public extension String {
+ var percentEncodedPageTitleForPathComponents: String? {
+ return denormalizedPageTitle?.addingPercentEncoding(withAllowedCharacters: .encodeURIComponentAllowed)
+ }
+
+ var normalizedPageTitle: String? {
+ return replacingOccurrences(of: "_", with: " ").precomposedStringWithCanonicalMapping
+ }
+
+ var denormalizedPageTitle: String? {
+ return replacingOccurrences(of: " ", with: "_").precomposedStringWithCanonicalMapping
+ }
+
+ var asTalkPageFragment: String? {
+ let denormalizedName = replacingOccurrences(of: " ", with: "_")
+ let unlinkedName = denormalizedName.replacingOccurrences(of: "[[", with: "").replacingOccurrences(of: "]]", with: "")
+ return unlinkedName.addingPercentEncoding(withAllowedCharacters: NSCharacterSet.wmf_encodeURIComponentAllowed())
+ }
+
+ // assumes string is already normalized
+ var googleFormPercentEncodedPageTitle: String? {
+ return googleFormPageTitle?.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
+ }
+
+ var googleFormPageTitle: String? {
+ return replacingOccurrences(of: " ", with: "+").precomposedStringWithCanonicalMapping
+ }
+
+ var unescapedNormalizedPageTitle: String? {
+ return removingPercentEncoding?.normalizedPageTitle
+ }
+
+ var isReferenceFragment: Bool {
+ return contains("ref_")
+ }
+
+ var isCitationFragment: Bool {
+ return contains("cite_note")
+ }
+
+ var isEndNoteFragment: Bool {
+ return contains("endnote_")
+ }
+}
+
+@objc extension NSString {
+ /// Deprecated - use namespace methods
+ @objc var wmf_isWikiResource: Bool {
+ return (self as String).wikiResourcePath != nil
+ }
+
+ /// Deprecated - use swift methods
+ @objc var wmf_pathWithoutWikiPrefix: String? {
+ return (self as String).wikiResourcePath
+ }
+
+ /// Deprecated - use swift methods
+ @objc var wmf_denormalizedPageTitle: String? {
+ return (self as String).denormalizedPageTitle
+ }
+
+ /// Deprecated - use swift methods
+ @objc var wmf_normalizedPageTitle: String? {
+ return (self as String).normalizedPageTitle
+ }
+
+ /// Deprecated - use swift methods
+ @objc var wmf_unescapedNormalizedPageTitle: String? {
+ return (self as String).unescapedNormalizedPageTitle
+ }
+
+ /// Deprecated - use swift methods
+ @objc var wmf_isReferenceFragment: Bool {
+ return (self as String).isReferenceFragment
+ }
+
+ /// Deprecated - use swift methods
+ @objc var wmf_isCitationFragment: Bool {
+ return (self as String).isCitationFragment
+ }
+
+ /// Deprecated - use swift methods
+ @objc var wmf_isEndNoteFragment: Bool {
+ return (self as String).isEndNoteFragment
+ }
+}
diff --git a/Apps/Wikipedia/WMF Framework/SummaryExtensions.swift b/Apps/Wikipedia/WMF Framework/SummaryExtensions.swift
new file mode 100644
index 0000000..63c9646
--- /dev/null
+++ b/Apps/Wikipedia/WMF Framework/SummaryExtensions.swift
@@ -0,0 +1,162 @@
+import CocoaLumberjackSwift
+
+extension WMFArticle {
+ func merge(_ article: WMFArticle) {
+ guard article.objectID != objectID else {
+ return
+ }
+ // merge important keys not set by the summary
+ let keysToMerge = [#keyPath(WMFArticle.savedDate), #keyPath(WMFArticle.placesSortOrder), #keyPath(WMFArticle.pageViews)]
+ for key in keysToMerge {
+ guard let valueToMerge = article.value(forKey: key) else {
+ continue
+ }
+ // keep the later date when both have date values
+ if let dateValueToMerge = valueToMerge as? Date, let dateValue = value(forKey: key) as? Date, dateValue > dateValueToMerge {
+ continue
+ }
+ // prefer existing values
+ if value(forKey: key) != nil {
+ continue
+ }
+ setValue(valueToMerge, forKey: key)
+ }
+
+ if let articleReadingLists = article.readingLists {
+ addReadingLists(articleReadingLists)
+ }
+
+ if let articlePreviewReadingLists = article.previewReadingLists {
+ addPreviewReadingLists(articlePreviewReadingLists)
+ }
+
+ if article.isExcludedFromFeed {
+ isExcludedFromFeed = true
+ }
+
+ let mergeViewedProperties: Bool
+ if let viewedDateToMerge = article.viewedDate {
+ if let existingViewedDate = viewedDate, existingViewedDate > viewedDateToMerge {
+ mergeViewedProperties = false
+ } else {
+ mergeViewedProperties = true
+ }
+ } else {
+ mergeViewedProperties = false
+ }
+
+ if mergeViewedProperties {
+ viewedDate = article.viewedDate
+ viewedFragment = article.viewedFragment
+ viewedScrollPosition = article.viewedScrollPosition
+ wasSignificantlyViewed = article.wasSignificantlyViewed
+ }
+ }
+
+ @objc public func update(withSummary summary: ArticleSummary) {
+ if let original = summary.original {
+ imageURLString = original.source
+ imageWidth = NSNumber(value: original.width)
+ imageHeight = NSNumber(value: original.height)
+ } else {
+ imageURLString = nil
+ imageWidth = NSNumber(value: 0)
+ imageHeight = NSNumber(value: 0)
+ }
+
+ if let thumbnail = summary.thumbnail {
+ thumbnailURLString = thumbnail.source
+ thumbnailURL = thumbnail.url
+ }
+
+ wikidataDescription = summary.articleDescription
+ wikidataID = summary.wikidataID
+ displayTitleHTML = summary.displayTitle ?? summary.title ?? ""
+ snippet = summary.extract?.wmf_summaryFromText()
+
+ if let summaryCoordinate = summary.coordinates {
+ coordinate = CLLocationCoordinate2D(latitude: summaryCoordinate.lat, longitude: summaryCoordinate.lon)
+ } else {
+ coordinate = nil
+ }
+ if let id = summary.id {
+ pageID = NSNumber(value: id)
+ } else {
+ pageID = nil
+ }
+ if let timestamp = summary.timestamp {
+ lastModifiedDate = DateFormatter.wmf_iso8601()?.date(from: timestamp)
+ } else {
+ lastModifiedDate = nil
+ }
+ }
+}
+
+extension NSManagedObjectContext {
+ @objc public func wmf_createOrUpdateArticleSummmaries(withSummaryResponses summaryResponses: [WMFInMemoryURLKey: ArticleSummary]) throws -> [WMFInMemoryURLKey: WMFArticle] {
+ guard !summaryResponses.isEmpty else {
+ return [:]
+ }
+ var keys: [WMFInMemoryURLKey] = []
+ var reverseRedirectedKeys: [WMFInMemoryURLKey: WMFInMemoryURLKey] = [:]
+ keys.reserveCapacity(summaryResponses.count)
+ for (key, summary) in summaryResponses {
+ guard
+ let summaryKey = summary.key,
+ key != summaryKey // find the mismatched keys
+ else {
+ keys.append(key)
+ continue
+ }
+ reverseRedirectedKeys[summaryKey] = key
+ keys.append(summaryKey)
+ do {
+ let articlesWithKey = try fetchArticles(with: key.url)
+ let articlesWithSummaryKey = try fetchArticles(with: summaryKey.url)
+ guard let canonicalArticle = articlesWithSummaryKey.first ?? articlesWithKey.first else {
+ continue
+ }
+ for article in articlesWithKey {
+ canonicalArticle.merge(article)
+ delete(article)
+ }
+ for article in articlesWithSummaryKey {
+ canonicalArticle.merge(article)
+ delete(article)
+ }
+ canonicalArticle.key = summaryKey.databaseKey
+ canonicalArticle.variant = summaryKey.languageVariantCode
+ } catch let error {
+ DDLogError("Error fetching articles for merge: \(error)")
+ }
+ }
+ var keysToCreate = Set(keys)
+ var articles: [WMFInMemoryURLKey: WMFArticle] = [:]
+ articles.reserveCapacity(keys.count)
+ let fetchedArticles = try self.fetchArticlesWithInMemoryURLKeys(keys)
+ for articleToUpdate in fetchedArticles {
+ guard let articleKey = articleToUpdate.inMemoryKey else {
+ continue
+ }
+ let requestedKey = reverseRedirectedKeys[articleKey] ?? articleKey
+ guard let result = summaryResponses[requestedKey] else {
+ articles[requestedKey] = articleToUpdate
+ continue
+ }
+ articleToUpdate.update(withSummary: result)
+ articles[requestedKey] = articleToUpdate
+ keysToCreate.remove(articleKey)
+ }
+ for key in keysToCreate {
+ let requestedKey = reverseRedirectedKeys[key] ?? key
+ guard let result = summaryResponses[requestedKey], // responses are by requested key
+ let article = self.createArticle(with: key.url) else { // article should have redirected key
+ continue
+ }
+ article.update(withSummary: result)
+ articles[requestedKey] = article
+ }
+ try self.save()
+ return articles
+ }
+}
diff --git a/Apps/Wikipedia/WMF Framework/SurveyAnnouncementsController.swift b/Apps/Wikipedia/WMF Framework/SurveyAnnouncementsController.swift
new file mode 100644
index 0000000..88e39c5
--- /dev/null
+++ b/Apps/Wikipedia/WMF Framework/SurveyAnnouncementsController.swift
@@ -0,0 +1,119 @@
+import Foundation
+
+@objc(WMFSurveyAnnouncementsController)
+public final class SurveyAnnouncementsController: NSObject {
+
+ @objc public static let shared = SurveyAnnouncementsController()
+
+ private let queue = DispatchQueue(label: "SurveyAnnouncementsQueue")
+
+ // ex: 'en.wikipedia.org'
+ typealias AnnouncementsHost = String
+ private var announcementsByHost: [AnnouncementsHost: [WMFAnnouncement]] = [:]
+
+ public struct SurveyAnnouncementResult {
+
+ public let campaignIdentifier: String
+ public let announcement: WMFAnnouncement
+ public let actionURLString: String
+ public let displayDelay: TimeInterval
+ }
+
+ public private(set) var failureDeterminingABTestBucket = false
+
+ @objc public func setAnnouncements(_ announcements: [WMFAnnouncement], forSiteURL siteURL: URL, dataStore: MWKDataStore) {
+
+ guard let components = URLComponents(url: siteURL, resolvingAgainstBaseURL: false),
+ let host = components.host else {
+ return
+ }
+
+ let surveyAnnouncements = announcements.filter { $0.announcementType == .survey }
+
+ queue.sync {
+ announcementsByHost[host] = surveyAnnouncements
+ }
+
+ // assign and persist ab test bucket & percentage
+ // this works for now since we only have one experiment for this release but will likely need to change as we expand
+ if let articleAsLivingDocAnnouncement = surveyAnnouncements.first(where: { ($0.identifier?.hasPrefix("IOSAAALDSURVEY")) ?? false }),
+ let percentage = articleAsLivingDocAnnouncement.percentReceivingExperiment {
+
+ do {
+ if dataStore.abTestsController.percentageForExperiment(.articleAsLivingDoc) == nil {
+ try dataStore.abTestsController.setPercentage(percentage, forExperiment: .articleAsLivingDoc)
+ }
+
+ try dataStore.abTestsController.determineBucketForExperiment(.articleAsLivingDoc, withPercentage: percentage)
+ failureDeterminingABTestBucket = false
+ } catch {
+ failureDeterminingABTestBucket = true
+ }
+ }
+ }
+
+ private func getAnnouncementsForSiteURL(_ siteURL: URL) -> [WMFAnnouncement]? {
+ guard let components = URLComponents(url: siteURL, resolvingAgainstBaseURL: false),
+ let host = components.host else {
+ return nil
+ }
+
+ var announcements: [WMFAnnouncement]? = []
+ queue.sync {
+ announcements = announcementsByHost[host]
+ }
+
+ return announcements
+ }
+
+ // Use for determining whether to show user a survey prompt or not.
+ // Considers domain, campaign start/end dates, and whether articleURL is within allowlist of article titles in campaign
+ public func activeSurveyAnnouncementResultForArticleURL(_ articleURL: URL) -> SurveyAnnouncementResult? {
+
+ guard let articleTitle = articleURL.wmf_title?.denormalizedPageTitle, let siteURL = articleURL.wmf_site else {
+ return nil
+ }
+
+ guard let announcements = getAnnouncementsForSiteURL(siteURL) else {
+ return nil
+ }
+
+ for announcement in announcements {
+
+ guard let startTime = announcement.startTime,
+ let endTime = announcement.endTime,
+ let domain = announcement.domain,
+ let articleTitles = announcement.articleTitles,
+ let displayDelay = announcement.displayDelay,
+ let components = URLComponents(url: siteURL, resolvingAgainstBaseURL: false),
+ let host = components.host,
+ let identifier = announcement.identifier,
+ let normalizedArticleTitle = articleTitle.normalizedPageTitle,
+ let actionURLString = announcement.actionURLString else {
+ continue
+ }
+
+ let now = Date()
+
+ if now > startTime && now < endTime && host == domain, articleTitles.contains(normalizedArticleTitle) {
+ return SurveyAnnouncementResult(campaignIdentifier: identifier, announcement: announcement, actionURLString: actionURLString, displayDelay: displayDelay.doubleValue)
+ }
+ }
+
+ return nil
+ }
+
+ public func userHasSeenSurveyPrompt(forCampaignIdentifier identifier: String) -> Bool {
+ // Note any value indicates survey was seen.
+ // true = they tapped through to the Google survey, false = they dismissed the survey prompt.
+ guard UserDefaults.standard.object(forKey: identifier) == nil else {
+ return true
+ }
+
+ return false
+ }
+
+ public func markSurveyAnnouncementAnswer(_ answer: Bool, campaignIdentifier: String) {
+ UserDefaults.standard.setValue(answer, forKey: campaignIdentifier)
+ }
+}
diff --git a/Apps/Wikipedia/WMF Framework/Theme.swift b/Apps/Wikipedia/WMF Framework/Theme.swift
new file mode 100644
index 0000000..17ed6a2
--- /dev/null
+++ b/Apps/Wikipedia/WMF Framework/Theme.swift
@@ -0,0 +1,1008 @@
+import Foundation
+import SystemConfiguration
+import Components
+
+public extension UIColor {
+ @objc(initWithHexInteger:alpha:)
+ convenience init(_ hex: Int, alpha: CGFloat) {
+ let r = CGFloat((hex & 0xFF0000) >> 16) / 255.0
+ let g = CGFloat((hex & 0xFF00) >> 8) / 255.0
+ let b = CGFloat(hex & 0xFF) / 255.0
+ self.init(red: r, green: g, blue: b, alpha: alpha)
+ }
+
+ @objc(initWithHexInteger:)
+ convenience init(_ hex: Int) {
+ self.init(hex, alpha: 1)
+ }
+
+ @objc class func wmf_colorWithHex(_ hex: Int) -> UIColor {
+ return UIColor(hex)
+ }
+
+ // `initWithHexString:alpha:` should almost never be used. `initWithHexInteger:alpha:` is preferred.
+ @objc(initWithHexString:alpha:)
+ convenience init(_ hexString: String, alpha: CGFloat = 1.0) {
+ let hex = hexString.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
+ guard hex.count == 6,
+ let int = Scanner(string: hex).scanInt32(representation: .hexadecimal),
+ int != UINT32_MAX else {
+ assertionFailure("Unexpected issue scanning hex string: \(hexString)")
+ self.init(white: 0, alpha: alpha)
+ return
+ }
+ self.init(Int(int), alpha: alpha)
+ }
+
+ // Make colors accessible to @objc
+ @objc static var wmf_blue_700: UIColor {
+ return .blue700
+ }
+
+ @objc static var wmf_blue_300: UIColor {
+ return .blue300
+ }
+
+ @objc static var wmf_blue_600: UIColor {
+ return .blue600
+ }
+
+ @objc static var wmf_yellow_600: UIColor {
+ return .yellow600
+ }
+ @objc static var wmf_red_600: UIColor {
+ return .red600
+ }
+
+ @objc static var wmf_gray_400: UIColor {
+ return .gray400
+ }
+
+ @objc static var wmf_green_600: UIColor {
+ return .green600
+ }
+
+ @objc static var wmf_purple: UIColor {
+ return .purple600
+ }
+
+ @objc static var wmf_orange: UIColor {
+ return .orange600
+ }
+
+ @objc func wmf_hexStringIncludingAlpha(_ includeAlpha: Bool) -> String {
+ var r: CGFloat = 0
+ var g: CGFloat = 0
+ var b: CGFloat = 0
+ var a: CGFloat = 0
+
+ getRed(&r, green: &g, blue: &b, alpha: &a)
+
+ var hexString = String(format: "%02X%02X%02X", Int(255.0 * r), Int(255.0 * g), Int(255.0 * b))
+ if includeAlpha {
+ hexString = hexString.appendingFormat("%02X", Int(255.0 * a))
+ }
+ return hexString
+ }
+
+ @objc var wmf_hexString: String {
+ return wmf_hexStringIncludingAlpha(false)
+ }
+}
+
+@objc(WMFColors)
+public class Colors: NSObject {
+ fileprivate static let light = Colors(
+ identifier: .light)
+
+ fileprivate static let sepia = Colors(identifier: .sepia)
+
+ fileprivate static let dark = Colors(identifier: .dark)
+
+ fileprivate static let black = Colors(identifier: .black)
+
+ fileprivate static let widgetLight = Colors(identifier: .widgetLight)
+
+ fileprivate static let widgetDark = Colors(identifier: .widgetDark)
+
+ public let identifier: Identifier
+
+ @objc public var baseBackground: UIColor {
+ switch identifier {
+ case .light, .widgetLight:
+ return .gray200
+ case .sepia:
+ return .beige400
+ case .dark, .black, .widgetDark:
+ return .gray800
+ }
+ }
+
+ @objc public var midBackground: UIColor {
+ switch identifier {
+ case .light, .widgetLight:
+ return .gray100
+ case .sepia:
+ return .beige300
+ case .dark, .black, .widgetDark:
+ return .gray700
+ }
+ }
+
+ @objc public var subCellBackground: UIColor {
+ switch identifier {
+ case .light:
+ return .white
+ case .sepia:
+ return .beige300
+ case .dark, .black:
+ return .gray700
+ case .widgetLight, .widgetDark:
+ return .clear
+ }
+ }
+
+ @objc public var paperBackground: UIColor {
+ switch identifier {
+ case .light, .widgetLight:
+ return .white
+ case .sepia:
+ return .beige100
+ case .dark:
+ return .gray675
+ case .black, .widgetDark:
+ return .black
+ }
+ }
+
+ @objc public var popoverBackground: UIColor {
+ switch identifier {
+ case .light, .sepia:
+ return .white
+ case .dark:
+ return .gray800
+ case .black:
+ return .gray700
+ case .widgetLight, .widgetDark:
+ return .clear
+ }
+ }
+
+ @objc public var chromeBackground: UIColor {
+ switch identifier {
+ case .light:
+ return .white
+ case .sepia:
+ return .beige100
+ case .dark:
+ return .gray700
+ case .black:
+ return .gray700
+ case .widgetLight, .widgetDark:
+ return .clear
+ }
+ }
+
+ @objc public var chromeShadow: UIColor {
+ switch identifier {
+ case .light, .widgetLight:
+ return .gray400
+ case .sepia:
+ return .taupe200
+ case .dark, .widgetDark:
+ return .gray650
+ case .black:
+ return .gray675
+ }
+ }
+
+ @objc public var overlayBackground: UIColor {
+ switch identifier {
+ case .light, .widgetLight:
+ return .black.withAlphaComponent(0.5)
+ case .sepia:
+ return .taupe600.withAlphaComponent(0.6)
+ default:
+ return .black.withAlphaComponent(0.75)
+ }
+ }
+
+ @objc public var batchSelectionBackground: UIColor {
+ switch identifier {
+ case .light, .sepia, .widgetLight:
+ return .blue100
+ case .dark, .black, .widgetDark:
+ return .blue700
+
+ }
+ }
+
+ @objc public var referenceHighlightBackground: UIColor {
+ switch identifier {
+ case .light, .sepia, .dark, .widgetLight:
+ return .clear
+ case .black, .widgetDark:
+ return .white.withAlphaComponent(0.2)
+ }
+ }
+
+ @objc public var hintBackground: UIColor {
+ switch identifier {
+ case .light, .sepia, .widgetLight:
+ return .blue100
+ case .dark, .widgetDark:
+ return .gray800
+ case .black:
+ return .gray650
+ }
+ }
+
+ @objc public var hintWarningText: UIColor {
+ switch identifier {
+ case .light, .sepia, .widgetLight:
+ return .gray700
+ case .dark, .black, .widgetDark:
+ return .yellow600
+ }
+ }
+
+ @objc public var hintWarningBackground: UIColor {
+ switch identifier {
+ case .light, .sepia:
+ return .orange600
+ case .dark, .black:
+ return .gray700
+ default:
+ return .clear
+ }
+ }
+
+ @objc public var animationBackground: UIColor {
+ switch identifier {
+ case .light, .sepia, .widgetLight:
+ return .gray150
+ case .dark, .black, .widgetDark:
+ return .gray700
+ }
+ }
+
+ @objc public var overlayText: UIColor {
+ return .gray600
+ }
+
+ @objc public var primaryText: UIColor {
+ switch identifier {
+ case .light, .sepia, .widgetLight:
+ return .gray700
+ case .dark, .black, .widgetDark:
+ return .gray100
+ }
+ }
+
+ @objc public var secondaryText: UIColor {
+ switch identifier {
+ case .light, .widgetLight:
+ return .gray500
+ case .sepia:
+ return .taupe600
+ default:
+ return .gray300
+ }
+ }
+
+ @objc public var tertiaryText: UIColor {
+ switch identifier {
+ case .light:
+ return .gray500
+ case .sepia:
+ return .taupe600
+ default:
+ return .gray300
+
+ }
+ }
+
+ @objc public var disabledText: UIColor {
+ switch identifier {
+ case .light:
+ return .gray500
+ case .sepia:
+ return .taupe600
+ default:
+ return .gray300
+ }
+ }
+
+ @objc public var disabledLink: UIColor {
+ switch identifier {
+ case .light, .widgetLight:
+ return .gray600
+ case .sepia:
+ return .gray500
+ case .dark, .black, .widgetDark:
+ return .gray400
+ }
+ }
+
+ @objc public var chromeText: UIColor {
+ switch identifier {
+ case .light, .sepia, .widgetLight:
+ return .gray700
+ case .dark, .black, .widgetDark:
+ return .gray100
+ }
+ }
+
+ @objc public var link: UIColor {
+ switch identifier {
+ case .light, .sepia, .widgetLight:
+ return .blue600
+ case .dark, .black, .widgetDark:
+ return .blue300
+ }
+ }
+
+ @objc public var accent: UIColor {
+ switch identifier {
+ default:
+ return .green600
+ }
+ }
+
+ @objc public var secondaryAction: UIColor {
+ return .blue700
+ }
+
+ @objc public var destructive: UIColor {
+ switch identifier {
+ case .sepia:
+ return .red700
+ default:
+ return .red600
+ }
+ }
+ @objc public var warning: UIColor {
+ switch identifier {
+ case .light, .sepia, .widgetLight:
+ return .orange600
+ case .dark, .black, .widgetDark:
+ return .yellow600
+ }
+ }
+ @objc public var error: UIColor {
+ switch identifier {
+ case .sepia:
+ return .red700
+ default:
+ return .red600
+ }
+ }
+ @objc public var unselected: UIColor {
+ switch identifier {
+ case .light, .widgetLight:
+ return .gray400
+ case .sepia:
+ return .taupe600
+ case .dark, .black, .widgetDark:
+ return .gray300
+ }
+ }
+
+ @objc public var border: UIColor {
+ switch identifier {
+ case .light, .widgetLight:
+ return .gray400
+ case .sepia:
+ return .taupe200
+ case .dark, .widgetDark:
+ return .gray650
+ case .black:
+ return .gray675
+ }
+ }
+
+ @objc public var shadow: UIColor {
+ switch identifier {
+ case .light, .widgetLight:
+ return .gray200
+ case .sepia:
+ return .taupe200
+ case .dark, .widgetDark:
+ return .gray800
+ case .black:
+ return .gray700
+ }
+ }
+
+ public var cardBackground: UIColor {
+ switch identifier {
+ case .light:
+ return .white
+ case .sepia:
+ return .beige300
+ case .dark, .black:
+ return .gray700
+ case .widgetLight, .widgetDark:
+ return .clear
+ }
+ }
+
+ public var selectedCardBackground: UIColor {
+ switch identifier {
+ case .light, .widgetLight:
+ return .gray100
+ case .sepia:
+ return .beige400
+ case .dark, .widgetDark:
+ return .gray700
+ case .black:
+ return .gray675
+ }
+ }
+
+ @objc public var cardBorder: UIColor {
+ switch identifier {
+ case .light, .widgetLight:
+ return .gray100
+ case .sepia:
+ return .taupe200
+ case .dark, .widgetDark:
+ return .gray650
+ case .black:
+ return .gray675
+ }
+ }
+
+ @objc public var cardShadow: UIColor {
+ switch identifier {
+ case .light:
+ return .gray700
+ default:
+ return .clear
+ }
+ }
+
+ @objc public var cardButtonBackground: UIColor {
+ switch identifier {
+ case .light, .widgetLight:
+ return .gray100
+ case .sepia:
+ return .beige300
+ case .dark, .black, .widgetDark:
+ return .gray650
+ }
+ }
+
+ @objc public var cardButtonSelectedBackground: UIColor {
+ switch identifier {
+ case .light, .widgetLight:
+ return .gray100
+ case .sepia:
+ return .taupe200
+ case .dark, .widgetDark:
+ return .gray650
+ case .black:
+ return .gray675
+ }
+ }
+
+ @objc public var icon: UIColor? {
+ switch identifier {
+ case .sepia:
+ return .taupe600
+ case .dark, .black:
+ return .gray300
+ default:
+ return nil
+ }
+ }
+
+ @objc public var iconBackground: UIColor? {
+ switch identifier {
+ case .sepia:
+ return .beige400
+ case .dark, .black:
+ return .gray675
+ default:
+ return nil
+ }
+ }
+
+ @objc public var searchFieldBackground: UIColor {
+ return .darkSearchFieldBackground
+ }
+
+ @objc public var keyboardBarSearchFieldBackground: UIColor {
+ switch identifier {
+ case .light, .sepia, .widgetLight:
+ return .gray200
+ case .dark, .black, .widgetDark:
+ return .gray650
+ }
+ }
+
+ @objc public var rankGradientStart: UIColor {
+ switch identifier {
+ case .light, .sepia, .widgetLight:
+ return .blue600
+ case .dark, .black, .widgetDark:
+ return .blue300
+ }
+ }
+
+ @objc public var rankGradientEnd: UIColor {
+ return .green600
+ }
+
+ @objc public var rankGradient: Gradient {
+ return Gradient(startColor: rankGradientStart, endColor: rankGradientEnd)
+ }
+
+ @objc public var blurEffectStyle: UIBlurEffect.Style {
+ switch identifier {
+ case .light, .sepia, .widgetLight:
+ return .extraLight
+ case .dark, .black, .widgetDark:
+ return .dark
+ }
+ }
+
+ @objc public var blurEffectBackground: UIColor {
+ switch identifier {
+ case .black, .dark, .widgetDark:
+ return .gray300.withAlphaComponent(0.55)
+ default:
+ return .clear
+ }
+ }
+
+ @objc public var tagText: UIColor {
+ switch identifier {
+ case .light:
+ return .blue600
+ default:
+ return .white
+ }
+ }
+
+ @objc public var tagBackground: UIColor {
+ switch identifier {
+ case .light:
+ return .blue600.withAlphaComponent(0.1)
+ default:
+ return .blue300
+
+ }
+ }
+
+ @objc public var tagSelectedBackground: UIColor {
+ switch identifier {
+ case .light:
+ return .blue600.withAlphaComponent(0.25)
+ default:
+ return .blue600
+
+ }
+ }
+
+ @objc public var distanceBorder: UIColor {
+ switch identifier {
+ case .light, .widgetLight:
+ return .gray400
+ case .sepia:
+ return .taupe600
+ case .dark, .black, .widgetDark:
+ return .gray300
+ }
+ }
+
+ @objc public var descriptionBackground: UIColor {
+ switch identifier {
+ case .light:
+ return .yellow600
+ case .sepia, .widgetLight:
+ return .orange600
+ case .dark, .black, .widgetDark:
+ return .blue300
+ }
+ }
+
+ @objc public var descriptionWarning: UIColor {
+ switch identifier {
+ case .light, .sepia, .widgetLight:
+ return .orange600
+ case .dark, .black, .widgetDark:
+ return .yellow600
+ }
+ }
+
+ @objc public var pageIndicator: UIColor {
+ return .blue100
+ }
+
+ @objc public var pageIndicatorCurrent: UIColor {
+ switch identifier {
+ case .light, .sepia, .widgetLight:
+ return .blue600
+ case .dark, .black, .widgetDark:
+ return .blue300
+ }
+ }
+
+ @objc public var unreadIndicator: UIColor {
+ return .green600
+ }
+
+ @objc public var refreshControlTint: UIColor {
+ return secondaryText
+ }
+
+ @objc public var inputAccessoryBackground: UIColor {
+ switch identifier {
+ case .light:
+ return .white
+ case .sepia:
+ return .beige300
+ case .dark, .black:
+ return .gray700
+ case .widgetLight, .widgetDark:
+ return .clear
+ }
+ }
+
+ @objc public var inputAccessoryButtonTint: UIColor {
+ switch identifier {
+ case .light, .sepia, .widgetLight:
+ return .gray600
+ case .dark, .black, .widgetDark:
+ return .gray100
+ }
+ }
+
+ @objc public var inputAccessoryButtonSelectedTint: UIColor {
+ return primaryText
+ }
+
+ @objc public var inputAccessoryButtonSelectedBackgroundColor: UIColor {
+ return baseBackground
+ }
+
+ public var diffTextAdd: UIColor {
+ switch identifier {
+ case .light:
+ return .gray700
+ default:
+ return .green600
+ }
+ }
+
+ public var diffTextDelete: UIColor {
+ switch identifier {
+ case .light:
+ return .gray700
+ case .sepia:
+ return.red700
+ case .dark, .black:
+ return .red600
+ default:
+ return .clear
+ }
+ }
+
+ public var diffHighlightAdd: UIColor? {
+ switch identifier {
+ case .light:
+ return .green100
+ default:
+ return nil
+ }
+ }
+
+ public var diffHighlightDelete: UIColor? {
+ switch identifier {
+ case .light:
+ return .red100
+ default:
+ return nil
+ }
+ }
+
+ public var diffStrikethroughColor: UIColor {
+ switch identifier {
+ case .light:
+ return .gray700
+ case .sepia:
+ return .red700
+ case .dark, .black:
+ return .red600
+ default:
+ return .clear
+ }
+ }
+
+ public var diffContextItemBackground: UIColor {
+ switch identifier {
+ case .light, .widgetLight:
+ return .gray100
+ case .sepia:
+ return .beige300
+ case .dark, .black, .widgetDark:
+ return .gray700
+ }
+ }
+
+ public var diffContextItemBorder: UIColor {
+ switch identifier {
+ case .light, .widgetLight:
+ return .gray400
+ case .sepia:
+ return .taupe200
+ case .dark, .widgetDark:
+ return .gray650
+ case .black:
+ return .gray675
+ }
+ }
+
+ public var diffMoveParagraphBackground: UIColor {
+ switch identifier {
+ case .light, .widgetLight:
+ return .gray100
+ case .sepia:
+ return .beige300
+ case .dark, .black, .widgetDark:
+ return .gray700
+ }
+ }
+
+ public var diffCompareAccent: UIColor {
+ return .orange600.withAlphaComponent(0.3)
+ }
+
+ public var diffCompareChangeHeading: UIColor {
+ switch identifier {
+ case .light:
+ return .white
+ case .sepia:
+ return .beige100
+ case .black, .dark:
+ return .black
+ default:
+ return .clear
+ }
+ }
+
+ public var talkPageCoffeRollBackground: UIColor {
+ switch identifier {
+ case .light:
+ return .beige100
+ case .sepia:
+ return .beige400
+ case .dark, .black:
+ return .gray800
+ default:
+ return .clear
+ }
+ }
+
+ init(identifier: Identifier) {
+ self.identifier = identifier
+ }
+
+ public enum Identifier {
+ case light
+ case sepia
+ case dark
+ case black
+ case widgetLight
+ case widgetDark
+ }
+
+}
+
+@objc(WMFTheme)
+public class Theme: NSObject {
+
+ @objc public static let standard = Theme.light
+
+ @objc public let colors: Colors
+
+ @objc public let isDark: Bool
+
+ @objc public let hasInputAccessoryShadow: Bool
+
+ @objc public var preferredStatusBarStyle: UIStatusBarStyle {
+ return isDark ? .lightContent : .default
+ }
+
+ @objc public var scrollIndicatorStyle: UIScrollView.IndicatorStyle {
+ return isDark ? .white : .black
+ }
+
+ @objc public var blurEffectStyle: UIBlurEffect.Style {
+ return isDark ? .dark : .light
+ }
+
+ @objc public var keyboardAppearance: UIKeyboardAppearance {
+ return isDark ? .dark : .light
+ }
+
+ @objc public lazy var navigationBarBackgroundImage: UIImage = {
+ return UIImage.wmf_image(from: colors.paperBackground)
+ }()
+
+ @objc public lazy var sheetNavigationBarBackgroundImage: UIImage = {
+ return UIImage.wmf_image(from: colors.chromeBackground)
+ }()
+
+ @objc public lazy var editorNavigationBarBackgroundImage: UIImage = {
+ return UIImage.wmf_image(from: colors.inputAccessoryBackground)
+ }()
+
+ @objc public var navigationBarShadowImage: UIImage {
+ return clearImage
+ }
+
+ @objc public lazy var clearImage: UIImage = {
+ return #imageLiteral(resourceName: "transparent-pixel")
+ }()
+
+ static let tabBarItemBadgeParagraphStyle: NSParagraphStyle = {
+ let paragraphStyle = NSMutableParagraphStyle()
+ paragraphStyle.firstLineHeadIndent = 0.4
+ return paragraphStyle
+ }()
+
+ static let tabBarItemFont: UIFont = {
+ return UIFont.systemFont(ofSize: 12)
+ }()
+
+ public lazy var tabBarItemBadgeTextAttributes: [NSAttributedString.Key: Any] = {
+ return [NSAttributedString.Key.foregroundColor: colors.chromeBackground, NSAttributedString.Key.paragraphStyle: Theme.tabBarItemBadgeParagraphStyle]
+ }()
+
+ public lazy var tabBarTitleTextAttributes: [NSAttributedString.Key: Any] = {
+ return [.foregroundColor: colors.secondaryText, .font: Theme.tabBarItemFont]
+ }()
+
+ public lazy var tabBarSelectedTitleTextAttributes: [NSAttributedString.Key: Any] = {
+ return [.foregroundColor: colors.link, .font: Theme.tabBarItemFont]
+ }()
+
+ public static let exploreCardCornerRadius: CGFloat = 10
+
+ static func roundedRectImage(with color: UIColor, cornerRadius: CGFloat, width: CGFloat? = nil, height: CGFloat? = nil) -> UIImage? {
+ let minDimension = 2 * cornerRadius + 1
+ let rect = CGRect(x: 0, y: 0, width: width ?? minDimension, height: height ?? minDimension)
+ let scale = UIScreen.main.scale
+ UIGraphicsBeginImageContextWithOptions(rect.size, false, scale)
+ guard let context = UIGraphicsGetCurrentContext() else {
+ return nil
+ }
+ context.setFillColor(color.cgColor)
+ let path = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius)
+ path.fill()
+ let capInsets = UIEdgeInsets(top: cornerRadius, left: cornerRadius, bottom: cornerRadius, right: cornerRadius)
+ let image = UIGraphicsGetImageFromCurrentImageContext()?.resizableImage(withCapInsets: capInsets)
+ UIGraphicsEndImageContext()
+ return image
+ }
+
+ @objc public lazy var searchFieldBackgroundImage: UIImage? = {
+ return Theme.roundedRectImage(with: colors.searchFieldBackground, cornerRadius: 10, height: 36)
+ }()
+
+ @objc public lazy var navigationBarTitleTextAttributes: [NSAttributedString.Key: Any] = {
+ return [NSAttributedString.Key.foregroundColor: colors.chromeText]
+ }()
+
+ public static let dimmedImageOpacity: CGFloat = 0.65
+ @objc public let imageOpacity: CGFloat
+ @objc public let cardBorderWidthInPixels: Int
+ @objc public let cardShadowOpacity: Float
+
+ @objc public let name: String
+ @objc public let displayName: String
+ public let analyticsName: String
+ public let webName: String
+
+ @objc public let multiSelectIndicatorImage: UIImage?
+ fileprivate static let lightMultiSelectIndicator = UIImage(named: "selected", in: Bundle.main, compatibleWith:nil)
+ fileprivate static let darkMultiSelectIndicator = UIImage(named: "selected-dark", in: Bundle.main, compatibleWith:nil)
+
+ private static let defaultCardBorderWidthInPixels: Int = 1
+ private static let lightCardBorderWidthInPixels: Int = {
+ return DeviceInfo.shared.isOlderDevice ? 4 : defaultCardBorderWidthInPixels
+ }()
+
+ private static let defaultCardShadowOpacity: Float = {
+ return DeviceInfo.shared.isOlderDevice ? 0 : 0.13
+ }()
+
+ @objc public static let defaultThemeName = "standard"
+ @objc public static let defaultAnalyticsThemeName = "default"
+
+ private static let darkThemePrefix = "dark"
+ private static let blackThemePrefix = "black"
+
+ @objc public static func isDefaultThemeName(_ name: String?) -> Bool {
+ guard let name = name else {
+ return true
+ }
+ return name == defaultThemeName
+ }
+
+ @objc public static func isDarkThemeName(_ name: String?) -> Bool {
+ guard let name = name else {
+ return false
+ }
+ return name.hasPrefix(darkThemePrefix) || name.hasPrefix(blackThemePrefix)
+ }
+
+ @objc public static let light = Theme(colors: .light, imageOpacity: 1, cardBorderWidthInPixels: Theme.lightCardBorderWidthInPixels, cardShadowOpacity: defaultCardShadowOpacity, multiSelectIndicatorImage: Theme.lightMultiSelectIndicator, isDark: false, hasInputAccessoryShadow: true, name: "light", displayName: WMFLocalizedString("theme-light-display-name", value: "Light", comment: "Light theme name presented to the user"), analyticsName: "default", webName: "light")
+
+ @objc public static let sepia = Theme(colors: .sepia, imageOpacity: 1, cardBorderWidthInPixels: Theme.defaultCardBorderWidthInPixels, cardShadowOpacity: 0, multiSelectIndicatorImage: Theme.lightMultiSelectIndicator, isDark: false, hasInputAccessoryShadow: false, name: "sepia", displayName: WMFLocalizedString("theme-sepia-display-name", value: "Sepia", comment: "Sepia theme name presented to the user"), analyticsName: "sepia", webName: "sepia")
+
+ @objc public static let dark = Theme(colors: .dark, imageOpacity: 1, cardBorderWidthInPixels: Theme.defaultCardBorderWidthInPixels, cardShadowOpacity: 0, multiSelectIndicatorImage: Theme.darkMultiSelectIndicator, isDark: true, hasInputAccessoryShadow: false, name: darkThemePrefix, displayName: WMFLocalizedString("theme-dark-display-name", value: "Dark", comment: "Dark theme name presented to the user"), analyticsName: "dark", webName: "dark")
+
+ @objc public static let darkDimmed = Theme(colors: .dark, imageOpacity: Theme.dimmedImageOpacity, cardBorderWidthInPixels: Theme.defaultCardBorderWidthInPixels, cardShadowOpacity: 0, multiSelectIndicatorImage: Theme.darkMultiSelectIndicator, isDark: true, hasInputAccessoryShadow: false, name: "\(darkThemePrefix)-dimmed", displayName: Theme.dark.displayName, analyticsName: "dark", webName: "dark")
+
+ @objc public static let black = Theme(colors: .black, imageOpacity: 1, cardBorderWidthInPixels: Theme.defaultCardBorderWidthInPixels, cardShadowOpacity: 0, multiSelectIndicatorImage: Theme.darkMultiSelectIndicator, isDark: true, hasInputAccessoryShadow: false, name: blackThemePrefix, displayName: WMFLocalizedString("theme-black-display-name", value: "Black", comment: "Black theme name presented to the user"), analyticsName: "black", webName: "black")
+
+ @objc public static let blackDimmed = Theme(colors: .black, imageOpacity: Theme.dimmedImageOpacity, cardBorderWidthInPixels: Theme.defaultCardBorderWidthInPixels, cardShadowOpacity: 0, multiSelectIndicatorImage: Theme.darkMultiSelectIndicator, isDark: true, hasInputAccessoryShadow: false, name: "\(blackThemePrefix)-dimmed", displayName: Theme.black.displayName, analyticsName: "black", webName: "black")
+
+ @objc public static let widgetLight = Theme(colors: .widgetLight, imageOpacity: 1, cardBorderWidthInPixels: Theme.defaultCardBorderWidthInPixels, cardShadowOpacity: 0, multiSelectIndicatorImage: nil, isDark: false, hasInputAccessoryShadow: false, name: "widget-light", displayName: "", analyticsName: "", webName: "light")
+
+ @objc public static let widgetDark = Theme(colors: .widgetDark, imageOpacity: 1, cardBorderWidthInPixels: Theme.defaultCardBorderWidthInPixels, cardShadowOpacity: 0, multiSelectIndicatorImage: nil, isDark: false, hasInputAccessoryShadow: false, name: "widget-dark", displayName: "", analyticsName: "", webName: "black")
+
+ public class func widgetThemeCompatible(with traitCollection: UITraitCollection) -> Theme {
+ return traitCollection.userInterfaceStyle == .dark ? Theme.widgetDark : Theme.widgetLight
+ }
+
+ init(colors: Colors, imageOpacity: CGFloat, cardBorderWidthInPixels: Int, cardShadowOpacity: Float, multiSelectIndicatorImage: UIImage?, isDark: Bool, hasInputAccessoryShadow: Bool, name: String, displayName: String, analyticsName: String, webName: String) {
+ self.colors = colors
+ self.imageOpacity = imageOpacity
+ self.name = name
+ self.displayName = displayName
+ self.multiSelectIndicatorImage = multiSelectIndicatorImage
+ self.isDark = isDark
+ self.hasInputAccessoryShadow = hasInputAccessoryShadow
+ self.cardBorderWidthInPixels = cardBorderWidthInPixels
+ self.cardShadowOpacity = cardShadowOpacity
+ self.analyticsName = analyticsName
+ self.webName = webName
+ }
+
+ fileprivate static let themesByName = [Theme.light.name: Theme.light, Theme.dark.name: Theme.dark, Theme.sepia.name: Theme.sepia, Theme.darkDimmed.name: Theme.darkDimmed, Theme.black.name: Theme.black, Theme.blackDimmed.name: Theme.blackDimmed]
+
+ @objc(withName:)
+ public class func withName(_ name: String?) -> Theme? {
+ guard let name = name else {
+ return nil
+ }
+ return themesByName[name]
+ }
+
+ @objc public func withDimmingEnabled(_ isDimmingEnabled: Bool) -> Theme {
+ guard let baseName = name.components(separatedBy: "-").first else {
+ return self
+ }
+ let adjustedName = isDimmingEnabled ? "\(baseName)-dimmed" : baseName
+ return Theme.withName(adjustedName) ?? self
+ }
+}
+
+@objc(WMFThemeable)
+public protocol Themeable: AnyObject {
+ @objc(applyTheme:)
+ func apply(theme: Theme) // this might be better as a var theme: Theme { get set } - common VC superclasses could check for viewIfLoaded and call an update method in the setter. This would elminate the need for the viewIfLoaded logic in every applyTheme:
+}
+
+// Use for SwiftUI environment objects
+public final class ObservableTheme: ObservableObject {
+ @Published public var theme: Theme
+
+ public init(theme: Theme) {
+ self.theme = theme
+ }
+}
diff --git a/Apps/Wikipedia/WMF Framework/ThemeableTextField.swift b/Apps/Wikipedia/WMF Framework/ThemeableTextField.swift
new file mode 100644
index 0000000..4c65676
--- /dev/null
+++ b/Apps/Wikipedia/WMF Framework/ThemeableTextField.swift
@@ -0,0 +1,92 @@
+import UIKit
+
+@objc(WMFThemeableTextField)
+open class ThemeableTextField: UITextField, Themeable {
+ var theme = Theme.light
+ @objc public var isUnderlined = true
+ private var clearButton: UIButton?
+ public var clearAccessibilityLabel: String? {
+ get {
+ return clearButton?.accessibilityLabel
+ } set {
+ clearButton?.accessibilityLabel = newValue
+ }
+ }
+
+ func setup() {
+ let image = #imageLiteral(resourceName: "clear-mini")
+ clearButton = UIButton(frame: CGRect(origin: .zero, size: image.size))
+ clearButton?.setImage(image, for: .normal)
+ clearButton?.addTarget(self, action: #selector(clear), for: .touchUpInside)
+ rightView = clearButton
+ rightViewMode = .whileEditing
+ textAlignment = .natural
+ }
+
+ override init(frame: CGRect) {
+ super.init(frame: frame)
+ setup()
+ }
+
+ required public init?(coder aDecoder: NSCoder) {
+ super.init(coder: aDecoder)
+ setup()
+ }
+
+ fileprivate var _placeholder: String?
+ override open var placeholder: String? {
+ didSet {
+ _placeholder = placeholder
+ guard let newPlaceholder = placeholder else {
+ return
+ }
+ attributedPlaceholder = NSAttributedString(string: newPlaceholder, attributes: [NSAttributedString.Key.foregroundColor: theme.colors.secondaryText])
+ }
+ }
+
+ fileprivate func _clear() {
+ text = nil
+ sendActions(for: .editingChanged)
+ }
+
+ @objc(clear)
+ fileprivate func clear() {
+ guard let shouldClear = delegate?.textFieldShouldClear?(self) else {
+ _clear()
+ return
+ }
+
+ guard shouldClear else {
+ return
+ }
+
+ _clear()
+ }
+
+ @objc(applyTheme:)
+ public func apply(theme: Theme) {
+ self.theme = theme
+ rightView?.tintColor = theme.colors.tertiaryText
+ backgroundColor = theme.colors.paperBackground
+ textColor = theme.colors.primaryText
+ placeholder = _placeholder
+ keyboardAppearance = theme.keyboardAppearance
+ borderStyle = .none
+ if isUnderlined {
+ layer.masksToBounds = false
+ layer.shadowColor = theme.colors.tertiaryText.cgColor
+ layer.shadowOffset = CGSize(width: 0.0, height: 1.0)
+ layer.shadowOpacity = 1.0
+ layer.shadowRadius = 0.0
+ } else {
+ layer.masksToBounds = true
+ layer.shadowColor = nil
+ layer.shadowOffset = CGSize.zero
+ layer.shadowOpacity = 0.0
+ layer.shadowRadius = 0.0
+ }
+
+ }
+}
+
+
diff --git a/Apps/Wikipedia/WMF Framework/Third Party/EXTScope.h b/Apps/Wikipedia/WMF Framework/Third Party/EXTScope.h
new file mode 100644
index 0000000..c7a51ef
--- /dev/null
+++ b/Apps/Wikipedia/WMF Framework/Third Party/EXTScope.h
@@ -0,0 +1,116 @@
+//
+// EXTScope.h
+// extobjc
+//
+// Created by Justin Spahr-Summers on 2011-05-04.
+// Copyright (C) 2012 Justin Spahr-Summers.
+// Released under the MIT license.
+//
+
+#import
+
+/**
+ * \@onExit defines some code to be executed when the current scope exits. The
+ * code must be enclosed in braces and terminated with a semicolon, and will be
+ * executed regardless of how the scope is exited, including from exceptions,
+ * \c goto, \c return, \c break, and \c continue.
+ *
+ * Provided code will go into a block to be executed later. Keep this in mind as
+ * it pertains to memory management, restrictions on assignment, etc. Because
+ * the code is used within a block, \c return is a legal (though perhaps
+ * confusing) way to exit the cleanup block early.
+ *
+ * Multiple \@onExit statements in the same scope are executed in reverse
+ * lexical order. This helps when pairing resource acquisition with \@onExit
+ * statements, as it guarantees teardown in the opposite order of acquisition.
+ *
+ * @note This statement cannot be used within scopes defined without braces
+ * (like a one line \c if). In practice, this is not an issue, since \@onExit is
+ * a useless construct in such a case anyways.
+ */
+#define onExit \
+ext_keywordify \
+__strong ext_cleanupBlock_t metamacro_concat(ext_exitBlock_, __LINE__) __attribute__((cleanup(ext_executeCleanupBlock), unused)) = ^
+
+/**
+ * Creates \c __weak shadow variables for each of the variables provided as
+ * arguments, which can later be made strong again with #strongify.
+ *
+ * This is typically used to weakly reference variables in a block, but then
+ * ensure that the variables stay alive during the actual execution of the block
+ * (if they were live upon entry).
+ *
+ * See #strongify for an example of usage.
+ */
+#define weakify(...) \
+ext_keywordify \
+metamacro_foreach_cxt(ext_weakify_,, __weak, __VA_ARGS__)
+
+/**
+ * Like #weakify, but uses \c __unsafe_unretained instead, for targets or
+ * classes that do not support weak references.
+ */
+#define unsafeify(...) \
+ext_keywordify \
+metamacro_foreach_cxt(ext_weakify_,, __unsafe_unretained, __VA_ARGS__)
+
+/**
+ * Strongly references each of the variables provided as arguments, which must
+ * have previously been passed to #weakify.
+ *
+ * The strong references created will shadow the original variable names, such
+ * that the original names can be used without issue (and a significantly
+ * reduced risk of retain cycles) in the current scope.
+ *
+ * @code
+
+ id foo = [[NSObject alloc] init];
+ id bar = [[NSObject alloc] init];
+
+ @weakify(foo, bar);
+
+ // this block will not keep 'foo' or 'bar' alive
+ BOOL (^matchesFooOrBar)(id) = ^ BOOL (id obj){
+ // but now, upon entry, 'foo' and 'bar' will stay alive until the block has
+ // finished executing
+ @strongify(foo, bar);
+
+ return [foo isEqual:obj] || [bar isEqual:obj];
+ };
+
+ * @endcode
+ */
+#define strongify(...) \
+ext_keywordify \
+_Pragma("clang diagnostic push") \
+_Pragma("clang diagnostic ignored \"-Wshadow\"") \
+metamacro_foreach(ext_strongify_,, __VA_ARGS__) \
+_Pragma("clang diagnostic pop")
+
+/*** implementation details follow ***/
+typedef void (^ext_cleanupBlock_t)(void);
+
+void ext_executeCleanupBlock (__strong ext_cleanupBlock_t *block);
+
+#define ext_weakify_(INDEX, CONTEXT, VAR) \
+CONTEXT __typeof__(VAR) metamacro_concat(VAR, _weak_) = (VAR);
+
+#define ext_strongify_(INDEX, VAR) \
+__strong __typeof__(VAR) VAR = metamacro_concat(VAR, _weak_);
+
+// Details about the choice of backing keyword:
+//
+// The use of @try/@catch/@finally can cause the compiler to suppress
+// return-type warnings.
+// The use of @autoreleasepool {} is not optimized away by the compiler,
+// resulting in superfluous creation of autorelease pools.
+//
+// Since neither option is perfect, and with no other alternatives, the
+// compromise is to use @autorelease in DEBUG builds to maintain compiler
+// analysis, and to use @try/@catch otherwise to avoid insertion of unnecessary
+// autorelease pools.
+#if DEBUG
+#define ext_keywordify autoreleasepool {}
+#else
+#define ext_keywordify try {} @catch (...) {}
+#endif
diff --git a/Apps/Wikipedia/WMF Framework/Third Party/EXTScope.m b/Apps/Wikipedia/WMF Framework/Third Party/EXTScope.m
new file mode 100644
index 0000000..5e68a9a
--- /dev/null
+++ b/Apps/Wikipedia/WMF Framework/Third Party/EXTScope.m
@@ -0,0 +1,15 @@
+//
+// EXTScope.m
+// extobjc
+//
+// Created by Justin Spahr-Summers on 2011-05-04.
+// Copyright (C) 2012 Justin Spahr-Summers.
+// Released under the MIT license.
+//
+
+#import "EXTScope.h"
+
+void ext_executeCleanupBlock (__strong ext_cleanupBlock_t *block) {
+ (*block)();
+}
+
diff --git a/Apps/Wikipedia/WMF Framework/Third Party/FLAnimatedImage/FLAnimatedImage/FLAnimatedImage.h b/Apps/Wikipedia/WMF Framework/Third Party/FLAnimatedImage/FLAnimatedImage/FLAnimatedImage.h
new file mode 100644
index 0000000..f056c3b
--- /dev/null
+++ b/Apps/Wikipedia/WMF Framework/Third Party/FLAnimatedImage/FLAnimatedImage/FLAnimatedImage.h
@@ -0,0 +1,84 @@
+//
+// FLAnimatedImage.h
+// Flipboard
+//
+// Created by Raphael Schaad on 7/8/13.
+// Copyright (c) 2013-2015 Flipboard. All rights reserved.
+//
+
+
+#import
+
+// Allow user classes conveniently just importing one header.
+#import "FLAnimatedImageView.h"
+
+
+#ifndef NS_DESIGNATED_INITIALIZER
+ #if __has_attribute(objc_designated_initializer)
+ #define NS_DESIGNATED_INITIALIZER __attribute((objc_designated_initializer))
+ #else
+ #define NS_DESIGNATED_INITIALIZER
+ #endif
+#endif
+
+extern const NSTimeInterval kFLAnimatedImageDelayTimeIntervalMinimum;
+
+//
+// An `FLAnimatedImage`'s job is to deliver frames in a highly performant way and works in conjunction with `FLAnimatedImageView`.
+// It subclasses `NSObject` and not `UIImage` because it's only an "image" in the sense that a sea lion is a lion.
+// It tries to intelligently choose the frame cache size depending on the image and memory situation with the goal to lower CPU usage for smaller ones, lower memory usage for larger ones and always deliver frames for high performant play-back.
+// Note: `posterImage`, `size`, `loopCount`, `delayTimes` and `frameCount` don't change after successful initialization.
+//
+@interface FLAnimatedImage : NSObject
+
+@property (nonatomic, strong, readonly) UIImage *posterImage; // Guaranteed to be loaded; usually equivalent to `-imageLazilyCachedAtIndex:0`
+@property (nonatomic, assign, readonly) CGSize size; // The `.posterImage`'s `.size`
+
+@property (nonatomic, assign, readonly) NSUInteger loopCount; // 0 means repeating the animation indefinitely
+@property (nonatomic, strong, readonly) NSDictionary *delayTimesForIndexes; // Of type `NSTimeInterval` boxed in `NSNumber`s
+@property (nonatomic, assign, readonly) NSUInteger frameCount; // Number of valid frames; equal to `[.delayTimes count]`
+
+@property (nonatomic, assign, readonly) NSUInteger frameCacheSizeCurrent; // Current size of intelligently chosen buffer window; can range in the interval [1..frameCount]
+@property (nonatomic, assign) NSUInteger frameCacheSizeMax; // Allow to cap the cache size; 0 means no specific limit (default)
+
+// Intended to be called from main thread synchronously; will return immediately.
+// If the result isn't cached, will return `nil`; the caller should then pause playback, not increment frame counter and keep polling.
+// After an initial loading time, depending on `frameCacheSize`, frames should be available immediately from the cache.
+- (UIImage *)imageLazilyCachedAtIndex:(NSUInteger)index;
+
+// Pass either a `UIImage` or an `FLAnimatedImage` and get back its size
++ (CGSize)sizeForImage:(id)image;
+
+// On success, the initializers return an `FLAnimatedImage` with all fields initialized, on failure they return `nil` and an error will be logged.
+- (instancetype)initWithAnimatedGIFData:(NSData *)data;
+// Pass 0 for optimalFrameCacheSize to get the default, predrawing is enabled by default.
+- (instancetype)initWithAnimatedGIFData:(NSData *)data optimalFrameCacheSize:(NSUInteger)optimalFrameCacheSize predrawingEnabled:(BOOL)isPredrawingEnabled NS_DESIGNATED_INITIALIZER;
++ (instancetype)animatedImageWithGIFData:(NSData *)data;
+
+@property (nonatomic, strong, readonly) NSData *data; // The data the receiver was initialized with; read-only
+
+@end
+
+typedef NS_ENUM(NSUInteger, FLLogLevel) {
+ FLLogLevelNone = 0,
+ FLLogLevelError,
+ FLLogLevelWarn,
+ FLLogLevelInfo,
+ FLLogLevelDebug,
+ FLLogLevelVerbose
+};
+
+@interface FLAnimatedImage (Logging)
+
++ (void)setLogBlock:(void (^)(NSString *logString, FLLogLevel logLevel))logBlock logLevel:(FLLogLevel)logLevel;
++ (void)logStringFromBlock:(NSString *(^)(void))stringBlock withLevel:(FLLogLevel)level;
+
+@end
+
+#define FLLog(logLevel, format, ...) [FLAnimatedImage logStringFromBlock:^NSString *{ return [NSString stringWithFormat:(format), ## __VA_ARGS__]; } withLevel:(logLevel)]
+
+@interface FLWeakProxy : NSProxy
+
++ (instancetype)weakProxyForObject:(id)targetObject;
+
+@end
diff --git a/Apps/Wikipedia/WMF Framework/Third Party/FLAnimatedImage/FLAnimatedImage/FLAnimatedImage.m b/Apps/Wikipedia/WMF Framework/Third Party/FLAnimatedImage/FLAnimatedImage/FLAnimatedImage.m
new file mode 100755
index 0000000..402bb6f
--- /dev/null
+++ b/Apps/Wikipedia/WMF Framework/Third Party/FLAnimatedImage/FLAnimatedImage/FLAnimatedImage.m
@@ -0,0 +1,820 @@
+//
+// FLAnimatedImage.m
+// Flipboard
+//
+// Created by Raphael Schaad on 7/8/13.
+// Copyright (c) 2013-2015 Flipboard. All rights reserved.
+//
+
+
+#import "FLAnimatedImage.h"
+#import
+#import
+
+
+// From vm_param.h, define for iOS 8.0 or higher to build on device.
+#ifndef BYTE_SIZE
+ #define BYTE_SIZE 8 // byte size in bits
+#endif
+
+#define MEGABYTE (1024 * 1024)
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Weverything"
+
+// This is how the fastest browsers do it as per 2012: http://nullsleep.tumblr.com/post/16524517190/animated-gif-minimum-frame-delay-browser-compatibility
+const NSTimeInterval kFLAnimatedImageDelayTimeIntervalMinimum = 0.02;
+
+// An animated image's data size (dimensions * frameCount) category; its value is the max allowed memory (in MB).
+// E.g.: A 100x200px GIF with 30 frames is ~2.3MB in our pixel format and would fall into the `FLAnimatedImageDataSizeCategoryAll` category.
+typedef NS_ENUM(NSUInteger, FLAnimatedImageDataSizeCategory) {
+ FLAnimatedImageDataSizeCategoryAll = 10, // All frames permanently in memory (be nice to the CPU)
+ FLAnimatedImageDataSizeCategoryDefault = 75, // A frame cache of default size in memory (usually real-time performance and keeping low memory profile)
+ FLAnimatedImageDataSizeCategoryOnDemand = 250, // Only keep one frame at the time in memory (easier on memory, slowest performance)
+ FLAnimatedImageDataSizeCategoryUnsupported // Even for one frame too large, computer says no.
+};
+
+typedef NS_ENUM(NSUInteger, FLAnimatedImageFrameCacheSize) {
+ FLAnimatedImageFrameCacheSizeNoLimit = 0, // 0 means no specific limit
+ FLAnimatedImageFrameCacheSizeLowMemory = 1, // The minimum frame cache size; this will produce frames on-demand.
+ FLAnimatedImageFrameCacheSizeGrowAfterMemoryWarning = 2, // If we can produce the frames faster than we consume, one frame ahead will already result in a stutter-free playback.
+ FLAnimatedImageFrameCacheSizeDefault = 5 // Build up a comfy buffer window to cope with CPU hiccups etc.
+};
+
+
+#if defined(DEBUG) && DEBUG
+@protocol FLAnimatedImageDebugDelegate
+@optional
+- (void)debug_animatedImage:(FLAnimatedImage *)animatedImage didUpdateCachedFrames:(NSIndexSet *)indexesOfFramesInCache;
+- (void)debug_animatedImage:(FLAnimatedImage *)animatedImage didRequestCachedFrame:(NSUInteger)index;
+- (CGFloat)debug_animatedImagePredrawingSlowdownFactor:(FLAnimatedImage *)animatedImage;
+@end
+#endif
+
+
+@interface FLAnimatedImage ()
+
+@property (nonatomic, assign, readonly) NSUInteger frameCacheSizeOptimal; // The optimal number of frames to cache based on image size & number of frames; never changes
+@property (nonatomic, assign, readonly, getter=isPredrawingEnabled) BOOL predrawingEnabled; // Enables predrawing of images to improve performance.
+@property (nonatomic, assign) NSUInteger frameCacheSizeMaxInternal; // Allow to cap the cache size e.g. when memory warnings occur; 0 means no specific limit (default)
+@property (nonatomic, assign) NSUInteger requestedFrameIndex; // Most recently requested frame index
+@property (nonatomic, assign, readonly) NSUInteger posterImageFrameIndex; // Index of non-purgable poster image; never changes
+@property (nonatomic, strong, readonly) NSMutableDictionary *cachedFramesForIndexes;
+@property (nonatomic, strong, readonly) NSMutableIndexSet *cachedFrameIndexes; // Indexes of cached frames
+@property (nonatomic, strong, readonly) NSMutableIndexSet *requestedFrameIndexes; // Indexes of frames that are currently produced in the background
+@property (nonatomic, strong, readonly) NSIndexSet *allFramesIndexSet; // Default index set with the full range of indexes; never changes
+@property (nonatomic, assign) NSUInteger memoryWarningCount;
+@property (nonatomic, strong, readonly) dispatch_queue_t serialQueue;
+@property (nonatomic, strong, readonly) __attribute__((NSObject)) CGImageSourceRef imageSource;
+
+// The weak proxy is used to break retain cycles with delayed actions from memory warnings.
+// We are lying about the actual type here to gain static type checking and eliminate casts.
+// The actual type of the object is `FLWeakProxy`.
+@property (nonatomic, strong, readonly) FLAnimatedImage *weakProxy;
+
+#if defined(DEBUG) && DEBUG
+@property (nonatomic, weak) id debug_delegate;
+#endif
+
+@end
+
+
+// For custom dispatching of memory warnings to avoid deallocation races since NSNotificationCenter doesn't retain objects it is notifying.
+static NSHashTable *allAnimatedImagesWeak;
+
+@implementation FLAnimatedImage
+
+#pragma mark - Accessors
+#pragma mark Public
+
+// This is the definite value the frame cache needs to size itself to.
+- (NSUInteger)frameCacheSizeCurrent
+{
+ NSUInteger frameCacheSizeCurrent = self.frameCacheSizeOptimal;
+
+ // If set, respect the caps.
+ if (self.frameCacheSizeMax > FLAnimatedImageFrameCacheSizeNoLimit) {
+ frameCacheSizeCurrent = MIN(frameCacheSizeCurrent, self.frameCacheSizeMax);
+ }
+
+ if (self.frameCacheSizeMaxInternal > FLAnimatedImageFrameCacheSizeNoLimit) {
+ frameCacheSizeCurrent = MIN(frameCacheSizeCurrent, self.frameCacheSizeMaxInternal);
+ }
+
+ return frameCacheSizeCurrent;
+}
+
+
+- (void)setFrameCacheSizeMax:(NSUInteger)frameCacheSizeMax
+{
+ if (_frameCacheSizeMax != frameCacheSizeMax) {
+
+ // Remember whether the new cap will cause the current cache size to shrink; then we'll make sure to purge from the cache if needed.
+ BOOL willFrameCacheSizeShrink = (frameCacheSizeMax < self.frameCacheSizeCurrent);
+
+ // Update the value
+ _frameCacheSizeMax = frameCacheSizeMax;
+
+ if (willFrameCacheSizeShrink) {
+ [self purgeFrameCacheIfNeeded];
+ }
+ }
+}
+
+
+#pragma mark Private
+
+- (void)setFrameCacheSizeMaxInternal:(NSUInteger)frameCacheSizeMaxInternal
+{
+ if (_frameCacheSizeMaxInternal != frameCacheSizeMaxInternal) {
+
+ // Remember whether the new cap will cause the current cache size to shrink; then we'll make sure to purge from the cache if needed.
+ BOOL willFrameCacheSizeShrink = (frameCacheSizeMaxInternal < self.frameCacheSizeCurrent);
+
+ // Update the value
+ _frameCacheSizeMaxInternal = frameCacheSizeMaxInternal;
+
+ if (willFrameCacheSizeShrink) {
+ [self purgeFrameCacheIfNeeded];
+ }
+ }
+}
+
+
+#pragma mark - Life Cycle
+
++ (void)initialize
+{
+ if (self == [FLAnimatedImage class]) {
+ // UIKit memory warning notification handler shared by all of the instances
+ allAnimatedImagesWeak = [NSHashTable weakObjectsHashTable];
+
+ [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidReceiveMemoryWarningNotification object:nil queue:nil usingBlock:^(NSNotification *note) {
+ // UIKit notifications are posted on the main thread. didReceiveMemoryWarning: is expecting the main run loop, and we don't lock on allAnimatedImagesWeak
+ NSAssert([NSThread isMainThread], @"Received memory warning on non-main thread");
+ // Get a strong reference to all of the images. If an instance is returned in this array, it is still live and has not entered dealloc.
+ // Note that FLAnimatedImages can be created on any thread, so the hash table must be locked.
+ NSArray *images = nil;
+ @synchronized(allAnimatedImagesWeak) {
+ images = [[allAnimatedImagesWeak allObjects] copy];
+ }
+ // Now issue notifications to all of the images while holding a strong reference to them
+ [images makeObjectsPerformSelector:@selector(didReceiveMemoryWarning:) withObject:note];
+ }];
+ }
+}
+
+
+- (instancetype)init
+{
+ FLAnimatedImage *animatedImage = [self initWithAnimatedGIFData:nil];
+ if (!animatedImage) {
+ FLLog(FLLogLevelError, @"Use `-initWithAnimatedGIFData:` and supply the animated GIF data as an argument to initialize an object of type `FLAnimatedImage`.");
+ }
+ return animatedImage;
+}
+
+
+- (instancetype)initWithAnimatedGIFData:(NSData *)data
+{
+ return [self initWithAnimatedGIFData:data optimalFrameCacheSize:0 predrawingEnabled:YES];
+}
+
+- (instancetype)initWithAnimatedGIFData:(NSData *)data optimalFrameCacheSize:(NSUInteger)optimalFrameCacheSize predrawingEnabled:(BOOL)isPredrawingEnabled
+{
+ // Early return if no data supplied!
+ BOOL hasData = ([data length] > 0);
+ if (!hasData) {
+ FLLog(FLLogLevelError, @"No animated GIF data supplied.");
+ return nil;
+ }
+
+ self = [super init];
+ if (self) {
+ // Do one-time initializations of `readonly` properties directly to ivar to prevent implicit actions and avoid need for private `readwrite` property overrides.
+
+ // Keep a strong reference to `data` and expose it read-only publicly.
+ // However, we will use the `_imageSource` as handler to the image data throughout our life cycle.
+ _data = data;
+ _predrawingEnabled = isPredrawingEnabled;
+
+ // Initialize internal data structures
+ _cachedFramesForIndexes = [[NSMutableDictionary alloc] init];
+ _cachedFrameIndexes = [[NSMutableIndexSet alloc] init];
+ _requestedFrameIndexes = [[NSMutableIndexSet alloc] init];
+
+ // Note: We could leverage `CGImageSourceCreateWithURL` too to add a second initializer `-initWithAnimatedGIFContentsOfURL:`.
+ _imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)data,
+ (__bridge CFDictionaryRef)@{(NSString *)kCGImageSourceShouldCache: @NO});
+ // Early return on failure!
+ if (!_imageSource) {
+ FLLog(FLLogLevelError, @"Failed to `CGImageSourceCreateWithData` for animated GIF data %@", data);
+ return nil;
+ }
+
+ // Early return if not GIF!
+ CFStringRef imageSourceContainerType = CGImageSourceGetType(_imageSource);
+ BOOL isGIFData = UTTypeConformsTo(imageSourceContainerType, kUTTypeGIF);
+ if (!isGIFData) {
+ FLLog(FLLogLevelError, @"Supplied data is of type %@ and doesn't seem to be GIF data %@", imageSourceContainerType, data);
+ return nil;
+ }
+
+ // Get `LoopCount`
+ // Note: 0 means repeating the animation indefinitely.
+ // Image properties example:
+ // {
+ // FileSize = 314446;
+ // "{GIF}" = {
+ // HasGlobalColorMap = 1;
+ // LoopCount = 0;
+ // };
+ // }
+ NSDictionary *imageProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyProperties(_imageSource, NULL);
+ _loopCount = [[[imageProperties objectForKey:(id)kCGImagePropertyGIFDictionary] objectForKey:(id)kCGImagePropertyGIFLoopCount] unsignedIntegerValue];
+
+ // Iterate through frame images
+ size_t imageCount = CGImageSourceGetCount(_imageSource);
+ NSUInteger skippedFrameCount = 0;
+ NSMutableDictionary *delayTimesForIndexesMutable = [NSMutableDictionary dictionaryWithCapacity:imageCount];
+ for (size_t i = 0; i < imageCount; i++) {
+ @autoreleasepool {
+ CGImageRef frameImageRef = CGImageSourceCreateImageAtIndex(_imageSource, i, NULL);
+ if (frameImageRef) {
+ UIImage *frameImage = [UIImage imageWithCGImage:frameImageRef];
+ // Check for valid `frameImage` before parsing its properties as frames can be corrupted (and `frameImage` even `nil` when `frameImageRef` was valid).
+ if (frameImage) {
+ // Set poster image
+ if (!self.posterImage) {
+ _posterImage = frameImage;
+ // Set its size to proxy our size.
+ _size = _posterImage.size;
+ // Remember index of poster image so we never purge it; also add it to the cache.
+ _posterImageFrameIndex = i;
+ [self.cachedFramesForIndexes setObject:self.posterImage forKey:@(self.posterImageFrameIndex)];
+ [self.cachedFrameIndexes addIndex:self.posterImageFrameIndex];
+ }
+
+ // Get `DelayTime`
+ // Note: It's not in (1/100) of a second like still falsely described in the documentation as per iOS 8 (rdar://19507384) but in seconds stored as `kCFNumberFloat32Type`.
+ // Frame properties example:
+ // {
+ // ColorModel = RGB;
+ // Depth = 8;
+ // PixelHeight = 960;
+ // PixelWidth = 640;
+ // "{GIF}" = {
+ // DelayTime = "0.4";
+ // UnclampedDelayTime = "0.4";
+ // };
+ // }
+
+ NSDictionary *frameProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(_imageSource, i, NULL);
+ NSDictionary *framePropertiesGIF = [frameProperties objectForKey:(id)kCGImagePropertyGIFDictionary];
+
+ // Try to use the unclamped delay time; fall back to the normal delay time.
+ NSNumber *delayTime = [framePropertiesGIF objectForKey:(id)kCGImagePropertyGIFUnclampedDelayTime];
+ if (!delayTime) {
+ delayTime = [framePropertiesGIF objectForKey:(id)kCGImagePropertyGIFDelayTime];
+ }
+ // If we don't get a delay time from the properties, fall back to `kDelayTimeIntervalDefault` or carry over the preceding frame's value.
+ const NSTimeInterval kDelayTimeIntervalDefault = 0.1;
+ if (!delayTime) {
+ if (i == 0) {
+ FLLog(FLLogLevelInfo, @"Falling back to default delay time for first frame %@ because none found in GIF properties %@", frameImage, frameProperties);
+ delayTime = @(kDelayTimeIntervalDefault);
+ } else {
+ FLLog(FLLogLevelInfo, @"Falling back to preceding delay time for frame %zu %@ because none found in GIF properties %@", i, frameImage, frameProperties);
+ delayTime = delayTimesForIndexesMutable[@(i - 1)];
+ }
+ }
+ // Support frame delays as low as `kFLAnimatedImageDelayTimeIntervalMinimum`, with anything below being rounded up to `kDelayTimeIntervalDefault` for legacy compatibility.
+ // To support the minimum even when rounding errors occur, use an epsilon when comparing. We downcast to float because that's what we get for delayTime from ImageIO.
+ if ([delayTime floatValue] < ((float)kFLAnimatedImageDelayTimeIntervalMinimum - FLT_EPSILON)) {
+ FLLog(FLLogLevelInfo, @"Rounding frame %zu's `delayTime` from %f up to default %f (minimum supported: %f).", i, [delayTime floatValue], kDelayTimeIntervalDefault, kFLAnimatedImageDelayTimeIntervalMinimum);
+ delayTime = @(kDelayTimeIntervalDefault);
+ }
+ delayTimesForIndexesMutable[@(i)] = delayTime;
+ } else {
+ skippedFrameCount++;
+ FLLog(FLLogLevelInfo, @"Dropping frame %zu because valid `CGImageRef` %@ did result in `nil`-`UIImage`.", i, frameImageRef);
+ }
+ CFRelease(frameImageRef);
+ } else {
+ skippedFrameCount++;
+ FLLog(FLLogLevelInfo, @"Dropping frame %zu because failed to `CGImageSourceCreateImageAtIndex` with image source %@", i, _imageSource);
+ }
+ }
+ }
+ _delayTimesForIndexes = [delayTimesForIndexesMutable copy];
+ _frameCount = imageCount;
+
+ if (self.frameCount == 0) {
+ FLLog(FLLogLevelInfo, @"Failed to create any valid frames for GIF with properties %@", imageProperties);
+ return nil;
+ } else if (self.frameCount == 1) {
+ // Warn when we only have a single frame but return a valid GIF.
+ FLLog(FLLogLevelInfo, @"Created valid GIF but with only a single frame. Image properties: %@", imageProperties);
+ } else {
+ // We have multiple frames, rock on!
+ }
+
+ // If no value is provided, select a default based on the GIF.
+ if (optimalFrameCacheSize == 0) {
+ // Calculate the optimal frame cache size: try choosing a larger buffer window depending on the predicted image size.
+ // It's only dependent on the image size & number of frames and never changes.
+ CGFloat animatedImageDataSize = CGImageGetBytesPerRow(self.posterImage.CGImage) * self.size.height * (self.frameCount - skippedFrameCount) / MEGABYTE;
+ if (animatedImageDataSize <= FLAnimatedImageDataSizeCategoryAll) {
+ _frameCacheSizeOptimal = self.frameCount;
+ } else if (animatedImageDataSize <= FLAnimatedImageDataSizeCategoryDefault) {
+ // This value doesn't depend on device memory much because if we're not keeping all frames in memory we will always be decoding 1 frame up ahead per 1 frame that gets played and at this point we might as well just keep a small buffer just large enough to keep from running out of frames.
+ _frameCacheSizeOptimal = FLAnimatedImageFrameCacheSizeDefault;
+ } else {
+ // The predicted size exceeds the limits to build up a cache and we go into low memory mode from the beginning.
+ _frameCacheSizeOptimal = FLAnimatedImageFrameCacheSizeLowMemory;
+ }
+ } else {
+ // Use the provided value.
+ _frameCacheSizeOptimal = optimalFrameCacheSize;
+ }
+ // In any case, cap the optimal cache size at the frame count.
+ _frameCacheSizeOptimal = MIN(_frameCacheSizeOptimal, self.frameCount);
+
+ // Convenience/minor performance optimization; keep an index set handy with the full range to return in `-frameIndexesToCache`.
+ _allFramesIndexSet = [[NSIndexSet alloc] initWithIndexesInRange:NSMakeRange(0, self.frameCount)];
+
+ // See the property declarations for descriptions.
+ _weakProxy = (id)[FLWeakProxy weakProxyForObject:self];
+
+ // Register this instance in the weak table for memory notifications. The NSHashTable will clean up after itself when we're gone.
+ // Note that FLAnimatedImages can be created on any thread, so the hash table must be locked.
+ @synchronized(allAnimatedImagesWeak) {
+ [allAnimatedImagesWeak addObject:self];
+ }
+ }
+ return self;
+}
+
+
++ (instancetype)animatedImageWithGIFData:(NSData *)data
+{
+ FLAnimatedImage *animatedImage = [[FLAnimatedImage alloc] initWithAnimatedGIFData:data];
+ return animatedImage;
+}
+
+
+- (void)dealloc
+{
+ if (_weakProxy) {
+ [NSObject cancelPreviousPerformRequestsWithTarget:_weakProxy];
+ }
+
+ if (_imageSource) {
+ CFRelease(_imageSource);
+ }
+}
+
+
+#pragma mark - Public Methods
+
+// See header for more details.
+// Note: both consumer and producer are throttled: consumer by frame timings and producer by the available memory (max buffer window size).
+- (UIImage *)imageLazilyCachedAtIndex:(NSUInteger)index
+{
+ // Early return if the requested index is beyond bounds.
+ // Note: We're comparing an index with a count and need to bail on greater than or equal to.
+ if (index >= self.frameCount) {
+ FLLog(FLLogLevelWarn, @"Skipping requested frame %lu beyond bounds (total frame count: %lu) for animated image: %@", (unsigned long)index, (unsigned long)self.frameCount, self);
+ return nil;
+ }
+
+ // Remember requested frame index, this influences what we should cache next.
+ self.requestedFrameIndex = index;
+#if defined(DEBUG) && DEBUG
+ if ([self.debug_delegate respondsToSelector:@selector(debug_animatedImage:didRequestCachedFrame:)]) {
+ [self.debug_delegate debug_animatedImage:self didRequestCachedFrame:index];
+ }
+#endif
+
+ // Quick check to avoid doing any work if we already have all possible frames cached, a common case.
+ if ([self.cachedFrameIndexes count] < self.frameCount) {
+ // If we have frames that should be cached but aren't and aren't requested yet, request them.
+ // Exclude existing cached frames, frames already requested, and specially cached poster image.
+ NSMutableIndexSet *frameIndexesToAddToCacheMutable = [self frameIndexesToCache];
+ [frameIndexesToAddToCacheMutable removeIndexes:self.cachedFrameIndexes];
+ [frameIndexesToAddToCacheMutable removeIndexes:self.requestedFrameIndexes];
+ [frameIndexesToAddToCacheMutable removeIndex:self.posterImageFrameIndex];
+ NSIndexSet *frameIndexesToAddToCache = [frameIndexesToAddToCacheMutable copy];
+
+ // Asynchronously add frames to our cache.
+ if ([frameIndexesToAddToCache count] > 0) {
+ [self addFrameIndexesToCache:frameIndexesToAddToCache];
+ }
+ }
+
+ // Get the specified image.
+ UIImage *image = self.cachedFramesForIndexes[@(index)];
+
+ // Purge if needed based on the current playhead position.
+ [self purgeFrameCacheIfNeeded];
+
+ return image;
+}
+
+
+// Only called once from `-imageLazilyCachedAtIndex` but factored into its own method for logical grouping.
+- (void)addFrameIndexesToCache:(NSIndexSet *)frameIndexesToAddToCache
+{
+ // Order matters. First, iterate over the indexes starting from the requested frame index.
+ // Then, if there are any indexes before the requested frame index, do those.
+ NSRange firstRange = NSMakeRange(self.requestedFrameIndex, self.frameCount - self.requestedFrameIndex);
+ NSRange secondRange = NSMakeRange(0, self.requestedFrameIndex);
+ if (firstRange.length + secondRange.length != self.frameCount) {
+ FLLog(FLLogLevelWarn, @"Two-part frame cache range doesn't equal full range.");
+ }
+
+ // Add to the requested list before we actually kick them off, so they don't get into the queue twice.
+ [self.requestedFrameIndexes addIndexes:frameIndexesToAddToCache];
+
+ // Lazily create dedicated isolation queue.
+ if (!self.serialQueue) {
+ _serialQueue = dispatch_queue_create("com.flipboard.framecachingqueue", DISPATCH_QUEUE_SERIAL);
+ }
+
+ // Start streaming requested frames in the background into the cache.
+ // Avoid capturing self in the block as there's no reason to keep doing work if the animated image went away.
+ FLAnimatedImage * __weak weakSelf = self;
+ dispatch_async(self.serialQueue, ^{
+ // Produce and cache next needed frame.
+ void (^frameRangeBlock)(NSRange, BOOL *) = ^(NSRange range, BOOL *stop) {
+ // Iterate through contiguous indexes; can be faster than `enumerateIndexesInRange:options:usingBlock:`.
+ for (NSUInteger i = range.location; i < NSMaxRange(range); i++) {
+#if defined(DEBUG) && DEBUG
+ CFTimeInterval predrawBeginTime = CACurrentMediaTime();
+#endif
+ UIImage *image = [weakSelf imageAtIndex:i];
+#if defined(DEBUG) && DEBUG
+ CFTimeInterval predrawDuration = CACurrentMediaTime() - predrawBeginTime;
+ CFTimeInterval slowdownDuration = 0.0;
+ if ([self.debug_delegate respondsToSelector:@selector(debug_animatedImagePredrawingSlowdownFactor:)]) {
+ CGFloat predrawingSlowdownFactor = [self.debug_delegate debug_animatedImagePredrawingSlowdownFactor:self];
+ slowdownDuration = predrawDuration * predrawingSlowdownFactor - predrawDuration;
+ [NSThread sleepForTimeInterval:slowdownDuration];
+ }
+ FLLog(FLLogLevelVerbose, @"Predrew frame %lu in %f ms for animated image: %@", (unsigned long)i, (predrawDuration + slowdownDuration) * 1000, self);
+#endif
+ // The results get returned one by one as soon as they're ready (and not in batch).
+ // The benefits of having the first frames as quick as possible outweigh building up a buffer to cope with potential hiccups when the CPU suddenly gets busy.
+ if (image && weakSelf) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ weakSelf.cachedFramesForIndexes[@(i)] = image;
+ [weakSelf.cachedFrameIndexes addIndex:i];
+ [weakSelf.requestedFrameIndexes removeIndex:i];
+#if defined(DEBUG) && DEBUG
+ if ([weakSelf.debug_delegate respondsToSelector:@selector(debug_animatedImage:didUpdateCachedFrames:)]) {
+ [weakSelf.debug_delegate debug_animatedImage:weakSelf didUpdateCachedFrames:weakSelf.cachedFrameIndexes];
+ }
+#endif
+ });
+ }
+ }
+ };
+
+ [frameIndexesToAddToCache enumerateRangesInRange:firstRange options:0 usingBlock:frameRangeBlock];
+ [frameIndexesToAddToCache enumerateRangesInRange:secondRange options:0 usingBlock:frameRangeBlock];
+ });
+}
+
+
++ (CGSize)sizeForImage:(id)image
+{
+ CGSize imageSize = CGSizeZero;
+
+ // Early return for nil
+ if (!image) {
+ return imageSize;
+ }
+
+ if ([image isKindOfClass:[UIImage class]]) {
+ UIImage *uiImage = (UIImage *)image;
+ imageSize = uiImage.size;
+ } else if ([image isKindOfClass:[FLAnimatedImage class]]) {
+ FLAnimatedImage *animatedImage = (FLAnimatedImage *)image;
+ imageSize = animatedImage.size;
+ } else {
+ // Bear trap to capture bad images; we have seen crashers cropping up on iOS 7.
+ FLLog(FLLogLevelError, @"`image` isn't of expected types `UIImage` or `FLAnimatedImage`: %@", image);
+ }
+
+ return imageSize;
+}
+
+
+#pragma mark - Private Methods
+#pragma mark Frame Loading
+
+- (UIImage *)imageAtIndex:(NSUInteger)index
+{
+ // It's very important to use the cached `_imageSource` since the random access to a frame with `CGImageSourceCreateImageAtIndex` turns from an O(1) into an O(n) operation when re-initializing the image source every time.
+ CGImageRef imageRef = CGImageSourceCreateImageAtIndex(_imageSource, index, NULL);
+
+ // Early return for nil
+ if (!imageRef) {
+ return nil;
+ }
+
+ UIImage *image = [UIImage imageWithCGImage:imageRef];
+ CFRelease(imageRef);
+
+ // Loading in the image object is only half the work, the displaying image view would still have to synchronosly wait and decode the image, so we go ahead and do that here on the background thread.
+ if (self.isPredrawingEnabled) {
+ image = [[self class] predrawnImageFromImage:image];
+ }
+
+ return image;
+}
+
+
+#pragma mark Frame Caching
+
+- (NSMutableIndexSet *)frameIndexesToCache
+{
+ NSMutableIndexSet *indexesToCache = nil;
+ // Quick check to avoid building the index set if the number of frames to cache equals the total frame count.
+ if (self.frameCacheSizeCurrent == self.frameCount) {
+ indexesToCache = [self.allFramesIndexSet mutableCopy];
+ } else {
+ indexesToCache = [[NSMutableIndexSet alloc] init];
+
+ // Add indexes to the set in two separate blocks- the first starting from the requested frame index, up to the limit or the end.
+ // The second, if needed, the remaining number of frames beginning at index zero.
+ NSUInteger firstLength = MIN(self.frameCacheSizeCurrent, self.frameCount - self.requestedFrameIndex);
+ NSRange firstRange = NSMakeRange(self.requestedFrameIndex, firstLength);
+ [indexesToCache addIndexesInRange:firstRange];
+ NSUInteger secondLength = self.frameCacheSizeCurrent - firstLength;
+ if (secondLength > 0) {
+ NSRange secondRange = NSMakeRange(0, secondLength);
+ [indexesToCache addIndexesInRange:secondRange];
+ }
+ // Double check our math, before we add the poster image index which may increase it by one.
+ if ([indexesToCache count] != self.frameCacheSizeCurrent) {
+ FLLog(FLLogLevelWarn, @"Number of frames to cache doesn't equal expected cache size.");
+ }
+
+ [indexesToCache addIndex:self.posterImageFrameIndex];
+ }
+
+ return indexesToCache;
+}
+
+
+- (void)purgeFrameCacheIfNeeded
+{
+ // Purge frames that are currently cached but don't need to be.
+ // But not if we're still under the number of frames to cache.
+ // This way, if all frames are allowed to be cached (the common case), we can skip all the `NSIndexSet` math below.
+ if ([self.cachedFrameIndexes count] > self.frameCacheSizeCurrent) {
+ NSMutableIndexSet *indexesToPurge = [self.cachedFrameIndexes mutableCopy];
+ [indexesToPurge removeIndexes:[self frameIndexesToCache]];
+ [indexesToPurge enumerateRangesUsingBlock:^(NSRange range, BOOL *stop) {
+ // Iterate through contiguous indexes; can be faster than `enumerateIndexesInRange:options:usingBlock:`.
+ for (NSUInteger i = range.location; i < NSMaxRange(range); i++) {
+ [self.cachedFrameIndexes removeIndex:i];
+ [self.cachedFramesForIndexes removeObjectForKey:@(i)];
+ // Note: Don't `CGImageSourceRemoveCacheAtIndex` on the image source for frames that we don't want cached any longer to maintain O(1) time access.
+#if defined(DEBUG) && DEBUG
+ if ([self.debug_delegate respondsToSelector:@selector(debug_animatedImage:didUpdateCachedFrames:)]) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ [self.debug_delegate debug_animatedImage:self didUpdateCachedFrames:self.cachedFrameIndexes];
+ });
+ }
+#endif
+ }
+ }];
+ }
+}
+
+
+- (void)growFrameCacheSizeAfterMemoryWarning:(NSNumber *)frameCacheSize
+{
+ self.frameCacheSizeMaxInternal = [frameCacheSize unsignedIntegerValue];
+ FLLog(FLLogLevelDebug, @"Grew frame cache size max to %lu after memory warning for animated image: %@", (unsigned long)self.frameCacheSizeMaxInternal, self);
+
+ // Schedule resetting the frame cache size max completely after a while.
+ const NSTimeInterval kResetDelay = 3.0;
+ [self.weakProxy performSelector:@selector(resetFrameCacheSizeMaxInternal) withObject:nil afterDelay:kResetDelay];
+}
+
+
+- (void)resetFrameCacheSizeMaxInternal
+{
+ self.frameCacheSizeMaxInternal = FLAnimatedImageFrameCacheSizeNoLimit;
+ FLLog(FLLogLevelDebug, @"Reset frame cache size max (current frame cache size: %lu) for animated image: %@", (unsigned long)self.frameCacheSizeCurrent, self);
+}
+
+
+#pragma mark System Memory Warnings Notification Handler
+
+- (void)didReceiveMemoryWarning:(NSNotification *)notification
+{
+ self.memoryWarningCount++;
+
+ // If we were about to grow larger, but got rapped on our knuckles by the system again, cancel.
+ [NSObject cancelPreviousPerformRequestsWithTarget:self.weakProxy selector:@selector(growFrameCacheSizeAfterMemoryWarning:) object:@(FLAnimatedImageFrameCacheSizeGrowAfterMemoryWarning)];
+ [NSObject cancelPreviousPerformRequestsWithTarget:self.weakProxy selector:@selector(resetFrameCacheSizeMaxInternal) object:nil];
+
+ // Go down to the minimum and by that implicitly immediately purge from the cache if needed to not get jettisoned by the system and start producing frames on-demand.
+ FLLog(FLLogLevelDebug, @"Attempt setting frame cache size max to %lu (previous was %lu) after memory warning #%lu for animated image: %@", (unsigned long)FLAnimatedImageFrameCacheSizeLowMemory, (unsigned long)self.frameCacheSizeMaxInternal, (unsigned long)self.memoryWarningCount, self);
+ self.frameCacheSizeMaxInternal = FLAnimatedImageFrameCacheSizeLowMemory;
+
+ // Schedule growing larger again after a while, but cap our attempts to prevent a periodic sawtooth wave (ramps upward and then sharply drops) of memory usage.
+ //
+ // [mem]^ (2) (5) (6) 1) Loading frames for the first time
+ // (*)| , , , 2) Mem warning #1; purge cache
+ // | /| (4)/| /| 3) Grow cache size a bit after a while, if no mem warning occurs
+ // | / | _/ | _/ | 4) Try to grow cache size back to optimum after a while, if no mem warning occurs
+ // |(1)/ |_/ |/ |__(7) 5) Mem warning #2; purge cache
+ // |__/ (3) 6) After repetition of (3) and (4), mem warning #3; purge cache
+ // +----------------------> 7) After 3 mem warnings, stay at minimum cache size
+ // [t]
+ // *) The mem high water mark before we get warned might change for every cycle.
+ //
+ const NSUInteger kGrowAttemptsMax = 2;
+ const NSTimeInterval kGrowDelay = 2.0;
+ if ((self.memoryWarningCount - 1) <= kGrowAttemptsMax) {
+ [self.weakProxy performSelector:@selector(growFrameCacheSizeAfterMemoryWarning:) withObject:@(FLAnimatedImageFrameCacheSizeGrowAfterMemoryWarning) afterDelay:kGrowDelay];
+ }
+
+ // Note: It's not possible to get the level of a memory warning with a public API: http://stackoverflow.com/questions/2915247/iphone-os-memory-warnings-what-do-the-different-levels-mean/2915477#2915477
+}
+
+
+#pragma mark Image Decoding
+
+// Decodes the image's data and draws it off-screen fully in memory; it's thread-safe and hence can be called on a background thread.
+// On success, the returned object is a new `UIImage` instance with the same content as the one passed in.
+// On failure, the returned object is the unchanged passed in one; the data will not be predrawn in memory though and an error will be logged.
+// First inspired by & good Karma to: https://gist.github.com/steipete/1144242
++ (UIImage *)predrawnImageFromImage:(UIImage *)imageToPredraw
+{
+ // Always use a device RGB color space for simplicity and predictability what will be going on.
+ CGColorSpaceRef colorSpaceDeviceRGBRef = CGColorSpaceCreateDeviceRGB();
+ // Early return on failure!
+ if (!colorSpaceDeviceRGBRef) {
+ FLLog(FLLogLevelError, @"Failed to `CGColorSpaceCreateDeviceRGB` for image %@", imageToPredraw);
+ return imageToPredraw;
+ }
+
+ // Even when the image doesn't have transparency, we have to add the extra channel because Quartz doesn't support other pixel formats than 32 bpp/8 bpc for RGB:
+ // kCGImageAlphaNoneSkipFirst, kCGImageAlphaNoneSkipLast, kCGImageAlphaPremultipliedFirst, kCGImageAlphaPremultipliedLast
+ // (source: docs "Quartz 2D Programming Guide > Graphics Contexts > Table 2-1 Pixel formats supported for bitmap graphics contexts")
+ size_t numberOfComponents = CGColorSpaceGetNumberOfComponents(colorSpaceDeviceRGBRef) + 1; // 4: RGB + A
+
+ // "In iOS 4.0 and later, and OS X v10.6 and later, you can pass NULL if you want Quartz to allocate memory for the bitmap." (source: docs)
+ void *data = NULL;
+ size_t width = imageToPredraw.size.width;
+ size_t height = imageToPredraw.size.height;
+ size_t bitsPerComponent = CHAR_BIT;
+
+ size_t bitsPerPixel = (bitsPerComponent * numberOfComponents);
+ size_t bytesPerPixel = (bitsPerPixel / BYTE_SIZE);
+ size_t bytesPerRow = (bytesPerPixel * width);
+
+ CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault;
+
+ CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(imageToPredraw.CGImage);
+ // If the alpha info doesn't match to one of the supported formats (see above), pick a reasonable supported one.
+ // "For bitmaps created in iOS 3.2 and later, the drawing environment uses the premultiplied ARGB format to store the bitmap data." (source: docs)
+ if (alphaInfo == kCGImageAlphaNone || alphaInfo == kCGImageAlphaOnly) {
+ alphaInfo = kCGImageAlphaNoneSkipFirst;
+ } else if (alphaInfo == kCGImageAlphaFirst) {
+ alphaInfo = kCGImageAlphaPremultipliedFirst;
+ } else if (alphaInfo == kCGImageAlphaLast) {
+ alphaInfo = kCGImageAlphaPremultipliedLast;
+ }
+ // "The constants for specifying the alpha channel information are declared with the `CGImageAlphaInfo` type but can be passed to this parameter safely." (source: docs)
+ bitmapInfo |= alphaInfo;
+
+ // Create our own graphics context to draw to; `UIGraphicsGetCurrentContext`/`UIGraphicsBeginImageContextWithOptions` doesn't create a new context but returns the current one which isn't thread-safe (e.g. main thread could use it at the same time).
+ // Note: It's not worth caching the bitmap context for multiple frames ("unique key" would be `width`, `height` and `hasAlpha`), it's ~50% slower. Time spent in libRIP's `CGSBlendBGRA8888toARGB8888` suddenly shoots up -- not sure why.
+ CGContextRef bitmapContextRef = CGBitmapContextCreate(data, width, height, bitsPerComponent, bytesPerRow, colorSpaceDeviceRGBRef, bitmapInfo);
+ CGColorSpaceRelease(colorSpaceDeviceRGBRef);
+ // Early return on failure!
+ if (!bitmapContextRef) {
+ FLLog(FLLogLevelError, @"Failed to `CGBitmapContextCreate` with color space %@ and parameters (width: %zu height: %zu bitsPerComponent: %zu bytesPerRow: %zu) for image %@", colorSpaceDeviceRGBRef, width, height, bitsPerComponent, bytesPerRow, imageToPredraw);
+ return imageToPredraw;
+ }
+
+ // Draw image in bitmap context and create image by preserving receiver's properties.
+ CGContextDrawImage(bitmapContextRef, CGRectMake(0.0, 0.0, imageToPredraw.size.width, imageToPredraw.size.height), imageToPredraw.CGImage);
+ CGImageRef predrawnImageRef = CGBitmapContextCreateImage(bitmapContextRef);
+ UIImage *predrawnImage = [UIImage imageWithCGImage:predrawnImageRef scale:imageToPredraw.scale orientation:imageToPredraw.imageOrientation];
+ CGImageRelease(predrawnImageRef);
+ CGContextRelease(bitmapContextRef);
+
+ // Early return on failure!
+ if (!predrawnImage) {
+ FLLog(FLLogLevelError, @"Failed to `imageWithCGImage:scale:orientation:` with image ref %@ created with color space %@ and bitmap context %@ and properties and properties (scale: %f orientation: %ld) for image %@", predrawnImageRef, colorSpaceDeviceRGBRef, bitmapContextRef, imageToPredraw.scale, (long)imageToPredraw.imageOrientation, imageToPredraw);
+ return imageToPredraw;
+ }
+
+ return predrawnImage;
+}
+
+
+#pragma mark - Description
+
+- (NSString *)description
+{
+ NSString *description = [super description];
+
+ description = [description stringByAppendingFormat:@" size=%@", NSStringFromCGSize(self.size)];
+ description = [description stringByAppendingFormat:@" frameCount=%lu", (unsigned long)self.frameCount];
+
+ return description;
+}
+
+
+@end
+
+#pragma mark - Logging
+
+@implementation FLAnimatedImage (Logging)
+
+static void (^_logBlock)(NSString *logString, FLLogLevel logLevel) = nil;
+static FLLogLevel _logLevel;
+
++ (void)setLogBlock:(void (^)(NSString *logString, FLLogLevel logLevel))logBlock logLevel:(FLLogLevel)logLevel
+{
+ _logBlock = logBlock;
+ _logLevel = logLevel;
+}
+
++ (void)logStringFromBlock:(NSString *(^)(void))stringBlock withLevel:(FLLogLevel)level
+{
+ if (level <= _logLevel && _logBlock && stringBlock) {
+ _logBlock(stringBlock(), level);
+ }
+}
+
+@end
+
+
+#pragma mark - FLWeakProxy
+
+@interface FLWeakProxy ()
+
+@property (nonatomic, weak) id target;
+
+@end
+
+
+@implementation FLWeakProxy
+
+#pragma mark Life Cycle
+
+// This is the designated creation method of an `FLWeakProxy` and
+// as a subclass of `NSProxy` it doesn't respond to or need `-init`.
++ (instancetype)weakProxyForObject:(id)targetObject
+{
+ FLWeakProxy *weakProxy = [FLWeakProxy alloc];
+ weakProxy.target = targetObject;
+ return weakProxy;
+}
+
+
+#pragma mark Forwarding Messages
+
+- (id)forwardingTargetForSelector:(SEL)selector
+{
+ // Keep it lightweight: access the ivar directly
+ return _target;
+}
+
+
+#pragma mark - NSWeakProxy Method Overrides
+#pragma mark Handling Unimplemented Methods
+
+- (void)forwardInvocation:(NSInvocation *)invocation
+{
+ // Fallback for when target is nil. Don't do anything, just return 0/NULL/nil.
+ // The method signature we've received to get here is just a dummy to keep `doesNotRecognizeSelector:` from firing.
+ // We can't really handle struct return types here because we don't know the length.
+ void *nullPointer = NULL;
+ [invocation setReturnValue:&nullPointer];
+}
+
+
+- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector
+{
+ // We only get here if `forwardingTargetForSelector:` returns nil.
+ // In that case, our weak target has been reclaimed. Return a dummy method signature to keep `doesNotRecognizeSelector:` from firing.
+ // We'll emulate the Obj-c messaging nil behavior by setting the return value to nil in `forwardInvocation:`, but we'll assume that the return value is `sizeof(void *)`.
+ // Other libraries handle this situation by making use of a global method signature cache, but that seems heavier than necessary and has issues as well.
+ // See https://www.mikeash.com/pyblog/friday-qa-2010-02-26-futures.html and https://github.com/steipete/PSTDelegateProxy/issues/1 for examples of using a method signature cache.
+ return [NSObject instanceMethodSignatureForSelector:@selector(init)];
+}
+
+
+@end
+#pragma clang diagnostic pop
diff --git a/Apps/Wikipedia/WMF Framework/Third Party/FLAnimatedImage/FLAnimatedImage/FLAnimatedImageView.h b/Apps/Wikipedia/WMF Framework/Third Party/FLAnimatedImage/FLAnimatedImage/FLAnimatedImageView.h
new file mode 100644
index 0000000..c0d527a
--- /dev/null
+++ b/Apps/Wikipedia/WMF Framework/Third Party/FLAnimatedImage/FLAnimatedImage/FLAnimatedImageView.h
@@ -0,0 +1,36 @@
+//
+// FLAnimatedImageView.h
+// Flipboard
+//
+// Created by Raphael Schaad on 7/8/13.
+// Copyright (c) 2013-2015 Flipboard. All rights reserved.
+//
+
+
+#import
+
+@class FLAnimatedImage;
+@protocol FLAnimatedImageViewDebugDelegate;
+
+
+//
+// An `FLAnimatedImageView` can take an `FLAnimatedImage` and plays it automatically when in view hierarchy and stops when removed.
+// The animation can also be controlled with the `UIImageView` methods `-start/stop/isAnimating`.
+// It is a fully compatible `UIImageView` subclass and can be used as a drop-in component to work with existing code paths expecting to display a `UIImage`.
+// Under the hood it uses a `CADisplayLink` for playback, which can be inspected with `currentFrame` & `currentFrameIndex`.
+//
+@interface FLAnimatedImageView : UIImageView
+
+// Setting `[UIImageView.image]` to a non-`nil` value clears out existing `animatedImage`.
+// And vice versa, setting `animatedImage` will initially populate the `[UIImageView.image]` to its `posterImage` and then start animating and hold `currentFrame`.
+@property (nonatomic, strong) FLAnimatedImage *animatedImage;
+@property (nonatomic, copy) void(^loopCompletionBlock)(NSUInteger loopCountRemaining);
+
+@property (nonatomic, strong, readonly) UIImage *currentFrame;
+@property (nonatomic, assign, readonly) NSUInteger currentFrameIndex;
+
+// The animation runloop mode. Enables playback during scrolling by allowing timer events (i.e. animation) with NSRunLoopCommonModes.
+// To keep scrolling smooth on single-core devices such as iPhone 3GS/4 and iPod Touch 4th gen, the default run loop mode is NSDefaultRunLoopMode. Otherwise, the default is NSDefaultRunLoopMode.
+@property (nonatomic, copy) NSString *runLoopMode;
+
+@end
diff --git a/Apps/Wikipedia/WMF Framework/Third Party/FLAnimatedImage/FLAnimatedImage/FLAnimatedImageView.m b/Apps/Wikipedia/WMF Framework/Third Party/FLAnimatedImage/FLAnimatedImage/FLAnimatedImageView.m
new file mode 100755
index 0000000..fe8b23c
--- /dev/null
+++ b/Apps/Wikipedia/WMF Framework/Third Party/FLAnimatedImage/FLAnimatedImage/FLAnimatedImageView.m
@@ -0,0 +1,441 @@
+//
+// FLAnimatedImageView.h
+// Flipboard
+//
+// Created by Raphael Schaad on 7/8/13.
+// Copyright (c) 2013-2015 Flipboard. All rights reserved.
+//
+
+
+#import "FLAnimatedImageView.h"
+#import "FLAnimatedImage.h"
+#import
+
+
+#if defined(DEBUG) && DEBUG
+@protocol FLAnimatedImageViewDebugDelegate
+@optional
+- (void)debug_animatedImageView:(FLAnimatedImageView *)animatedImageView waitingForFrame:(NSUInteger)index duration:(NSTimeInterval)duration;
+@end
+#endif
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Weverything"
+@interface FLAnimatedImageView ()
+
+// Override of public `readonly` properties as private `readwrite`
+@property (nonatomic, strong, readwrite) UIImage *currentFrame;
+@property (nonatomic, assign, readwrite) NSUInteger currentFrameIndex;
+
+@property (nonatomic, assign) NSUInteger loopCountdown;
+@property (nonatomic, assign) NSTimeInterval accumulator;
+@property (nonatomic, strong) CADisplayLink *displayLink;
+
+@property (nonatomic, assign) BOOL shouldAnimate; // Before checking this value, call `-updateShouldAnimate` whenever the animated image or visibility (window, superview, hidden, alpha) has changed.
+@property (nonatomic, assign) BOOL needsDisplayWhenImageBecomesAvailable;
+
+#if defined(DEBUG) && DEBUG
+@property (nonatomic, weak) id debug_delegate;
+#endif
+
+@end
+
+
+@implementation FLAnimatedImageView
+@synthesize runLoopMode = _runLoopMode;
+
+#pragma mark - Initializers
+
+// -initWithImage: isn't documented as a designated initializer of UIImageView, but it actually seems to be.
+// Using -initWithImage: doesn't call any of the other designated initializers.
+- (instancetype)initWithImage:(UIImage *)image
+{
+ self = [super initWithImage:image];
+ if (self) {
+ [self commonInit];
+ }
+ return self;
+}
+
+// -initWithImage:highlightedImage: also isn't documented as a designated initializer of UIImageView, but it doesn't call any other designated initializers.
+- (instancetype)initWithImage:(UIImage *)image highlightedImage:(UIImage *)highlightedImage
+{
+ self = [super initWithImage:image highlightedImage:highlightedImage];
+ if (self) {
+ [self commonInit];
+ }
+ return self;
+}
+
+- (instancetype)initWithFrame:(CGRect)frame
+{
+ self = [super initWithFrame:frame];
+ if (self) {
+ [self commonInit];
+ }
+ return self;
+}
+
+- (instancetype)initWithCoder:(NSCoder *)aDecoder
+{
+ self = [super initWithCoder:aDecoder];
+ if (self) {
+ [self commonInit];
+ }
+ return self;
+}
+
+- (void)commonInit
+{
+ self.runLoopMode = [[self class] defaultRunLoopMode];
+ self.accessibilityIgnoresInvertColors = YES;
+}
+
+
+#pragma mark - Accessors
+#pragma mark Public
+
+- (void)setAnimatedImage:(FLAnimatedImage *)animatedImage
+{
+ if (![_animatedImage isEqual:animatedImage]) {
+ if (animatedImage) {
+ // Clear out the image.
+ super.image = nil;
+ // Ensure disabled highlighting; it's not supported (see `-setHighlighted:`).
+ super.highlighted = NO;
+ // UIImageView seems to bypass some accessors when calculating its intrinsic content size, so this ensures its intrinsic content size comes from the animated image.
+ [self invalidateIntrinsicContentSize];
+ } else {
+ // Stop animating before the animated image gets cleared out.
+ [self stopAnimating];
+ }
+
+ _animatedImage = animatedImage;
+
+ self.currentFrame = animatedImage.posterImage;
+ self.currentFrameIndex = 0;
+ if (animatedImage.loopCount > 0) {
+ self.loopCountdown = animatedImage.loopCount;
+ } else {
+ self.loopCountdown = NSUIntegerMax;
+ }
+ self.accumulator = 0.0;
+
+ // Start animating after the new animated image has been set.
+ [self updateShouldAnimate];
+ if (self.shouldAnimate) {
+ [self startAnimating];
+ }
+
+ [self.layer setNeedsDisplay];
+ }
+}
+
+
+#pragma mark - Life Cycle
+
+- (void)dealloc
+{
+ // Removes the display link from all run loop modes.
+ [_displayLink invalidate];
+}
+
+
+#pragma mark - UIView Method Overrides
+#pragma mark Observing View-Related Changes
+
+- (void)didMoveToSuperview
+{
+ [super didMoveToSuperview];
+
+ [self updateShouldAnimate];
+ if (self.shouldAnimate) {
+ [self startAnimating];
+ } else {
+ [self stopAnimating];
+ }
+}
+
+
+- (void)didMoveToWindow
+{
+ [super didMoveToWindow];
+
+ [self updateShouldAnimate];
+ if (self.shouldAnimate) {
+ [self startAnimating];
+ } else {
+ [self stopAnimating];
+ }
+}
+
+- (void)setAlpha:(CGFloat)alpha
+{
+ [super setAlpha:alpha];
+
+ [self updateShouldAnimate];
+ if (self.shouldAnimate) {
+ [self startAnimating];
+ } else {
+ [self stopAnimating];
+ }
+}
+
+- (void)setHidden:(BOOL)hidden
+{
+ [super setHidden:hidden];
+
+ [self updateShouldAnimate];
+ if (self.shouldAnimate) {
+ [self startAnimating];
+ } else {
+ [self stopAnimating];
+ }
+}
+
+
+#pragma mark Auto Layout
+
+- (CGSize)intrinsicContentSize
+{
+ // Default to let UIImageView handle the sizing of its image, and anything else it might consider.
+ CGSize intrinsicContentSize = [super intrinsicContentSize];
+
+ // If we have have an animated image, use its image size.
+ // UIImageView's intrinsic content size seems to be the size of its image. The obvious approach, simply calling `-invalidateIntrinsicContentSize` when setting an animated image, results in UIImageView steadfastly returning `{UIViewNoIntrinsicMetric, UIViewNoIntrinsicMetric}` for its intrinsicContentSize.
+ // (Perhaps UIImageView bypasses its `-image` getter in its implementation of `-intrinsicContentSize`, as `-image` is not called after calling `-invalidateIntrinsicContentSize`.)
+ if (self.animatedImage) {
+ intrinsicContentSize = self.image.size;
+ }
+
+ return intrinsicContentSize;
+}
+
+#pragma mark Smart Invert Colors
+
+#pragma mark - UIImageView Method Overrides
+#pragma mark Image Data
+
+- (UIImage *)image
+{
+ UIImage *image = nil;
+ if (self.animatedImage) {
+ // Initially set to the poster image.
+ image = self.currentFrame;
+ } else {
+ image = super.image;
+ }
+ return image;
+}
+
+
+- (void)setImage:(UIImage *)image
+{
+ if (image) {
+ // Clear out the animated image and implicitly pause animation playback.
+ self.animatedImage = nil;
+ }
+
+ super.image = image;
+}
+
+
+#pragma mark Animating Images
+
+- (NSTimeInterval)frameDelayGreatestCommonDivisor
+{
+ // Presision is set to half of the `kFLAnimatedImageDelayTimeIntervalMinimum` in order to minimize frame dropping.
+ const NSTimeInterval kGreatestCommonDivisorPrecision = 2.0 / kFLAnimatedImageDelayTimeIntervalMinimum;
+
+ NSArray *delays = self.animatedImage.delayTimesForIndexes.allValues;
+
+ // Scales the frame delays by `kGreatestCommonDivisorPrecision`
+ // then converts it to an UInteger for in order to calculate the GCD.
+ NSUInteger scaledGCD = lrint([delays.firstObject floatValue] * kGreatestCommonDivisorPrecision);
+ for (NSNumber *value in delays) {
+ scaledGCD = gcd(lrint([value floatValue] * kGreatestCommonDivisorPrecision), scaledGCD);
+ }
+
+ // Reverse to scale to get the value back into seconds.
+ return scaledGCD / kGreatestCommonDivisorPrecision;
+}
+
+
+static NSUInteger gcd(NSUInteger a, NSUInteger b)
+{
+ // http://en.wikipedia.org/wiki/Greatest_common_divisor
+ if (a < b) {
+ return gcd(b, a);
+ } else if (a == b) {
+ return b;
+ }
+
+ while (true) {
+ NSUInteger remainder = a % b;
+ if (remainder == 0) {
+ return b;
+ }
+ a = b;
+ b = remainder;
+ }
+}
+
+
+- (void)startAnimating
+{
+ if (self.animatedImage) {
+ // Lazily create the display link.
+ if (!self.displayLink) {
+ // It is important to note the use of a weak proxy here to avoid a retain cycle. `-displayLinkWithTarget:selector:`
+ // will retain its target until it is invalidated. We use a weak proxy so that the image view will get deallocated
+ // independent of the display link's lifetime. Upon image view deallocation, we invalidate the display
+ // link which will lead to the deallocation of both the display link and the weak proxy.
+ FLWeakProxy *weakProxy = [FLWeakProxy weakProxyForObject:self];
+ self.displayLink = [CADisplayLink displayLinkWithTarget:weakProxy selector:@selector(displayDidRefresh:)];
+
+ [self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:self.runLoopMode];
+ }
+
+ // Note: The display link's `.frameInterval` value of 1 (default) means getting callbacks at the refresh rate of the display (~60Hz).
+ // Setting it to 2 divides the frame rate by 2 and hence calls back at every other display refresh.
+ const NSTimeInterval kDisplayRefreshRate = 60.0; // 60Hz
+ self.displayLink.frameInterval = MAX([self frameDelayGreatestCommonDivisor] * kDisplayRefreshRate, 1);
+
+ self.displayLink.paused = NO;
+ } else {
+ [super startAnimating];
+ }
+}
+
+- (void)setRunLoopMode:(NSString *)runLoopMode
+{
+ if (![@[NSDefaultRunLoopMode, NSRunLoopCommonModes] containsObject:runLoopMode]) {
+ NSAssert(NO, @"Invalid run loop mode: %@", runLoopMode);
+ _runLoopMode = [[self class] defaultRunLoopMode];
+ } else {
+ _runLoopMode = runLoopMode;
+ }
+}
+
+- (void)stopAnimating
+{
+ if (self.animatedImage) {
+ self.displayLink.paused = YES;
+ } else {
+ [super stopAnimating];
+ }
+}
+
+
+- (BOOL)isAnimating
+{
+ BOOL isAnimating = NO;
+ if (self.animatedImage) {
+ isAnimating = self.displayLink && !self.displayLink.isPaused;
+ } else {
+ isAnimating = [super isAnimating];
+ }
+ return isAnimating;
+}
+
+
+#pragma mark Highlighted Image Unsupport
+
+- (void)setHighlighted:(BOOL)highlighted
+{
+ // Highlighted image is unsupported for animated images, but implementing it breaks the image view when embedded in a UICollectionViewCell.
+ if (!self.animatedImage) {
+ [super setHighlighted:highlighted];
+ }
+}
+
+
+#pragma mark - Private Methods
+#pragma mark Animation
+
+// Don't repeatedly check our window & superview in `-displayDidRefresh:` for performance reasons.
+// Just update our cached value whenever the animated image or visibility (window, superview, hidden, alpha) is changed.
+- (void)updateShouldAnimate
+{
+ BOOL isVisible = self.window && self.superview && ![self isHidden] && self.alpha > 0.0;
+ self.shouldAnimate = self.animatedImage && isVisible;
+}
+
+
+- (void)displayDidRefresh:(CADisplayLink *)displayLink
+{
+ // If for some reason a wild call makes it through when we shouldn't be animating, bail.
+ // Early return!
+ if (!self.shouldAnimate) {
+ FLLog(FLLogLevelWarn, @"Trying to animate image when we shouldn't: %@", self);
+ return;
+ }
+
+ NSNumber *delayTimeNumber = [self.animatedImage.delayTimesForIndexes objectForKey:@(self.currentFrameIndex)];
+ // If we don't have a frame delay (e.g. corrupt frame), don't update the view but skip the playhead to the next frame (in else-block).
+ if (delayTimeNumber) {
+ NSTimeInterval delayTime = [delayTimeNumber floatValue];
+ // If we have a nil image (e.g. waiting for frame), don't update the view nor playhead.
+ UIImage *image = [self.animatedImage imageLazilyCachedAtIndex:self.currentFrameIndex];
+ if (image) {
+ FLLog(FLLogLevelVerbose, @"Showing frame %lu for animated image: %@", (unsigned long)self.currentFrameIndex, self.animatedImage);
+ self.currentFrame = image;
+ if (self.needsDisplayWhenImageBecomesAvailable) {
+ [self.layer setNeedsDisplay];
+ self.needsDisplayWhenImageBecomesAvailable = NO;
+ }
+
+ self.accumulator += displayLink.duration * displayLink.frameInterval;
+
+ // While-loop first inspired by & good Karma to: https://github.com/ondalabs/OLImageView/blob/master/OLImageView.m
+ while (self.accumulator >= delayTime) {
+ self.accumulator -= delayTime;
+ self.currentFrameIndex++;
+ if (self.currentFrameIndex >= self.animatedImage.frameCount) {
+ // If we've looped the number of times that this animated image describes, stop looping.
+ self.loopCountdown--;
+ if (self.loopCompletionBlock) {
+ self.loopCompletionBlock(self.loopCountdown);
+ }
+
+ if (self.loopCountdown == 0) {
+ [self stopAnimating];
+ return;
+ }
+ self.currentFrameIndex = 0;
+ }
+ // Calling `-setNeedsDisplay` will just paint the current frame, not the new frame that we may have moved to.
+ // Instead, set `needsDisplayWhenImageBecomesAvailable` to `YES` -- this will paint the new image once loaded.
+ self.needsDisplayWhenImageBecomesAvailable = YES;
+ }
+ } else {
+ FLLog(FLLogLevelDebug, @"Waiting for frame %lu for animated image: %@", (unsigned long)self.currentFrameIndex, self.animatedImage);
+#if defined(DEBUG) && DEBUG
+ if ([self.debug_delegate respondsToSelector:@selector(debug_animatedImageView:waitingForFrame:duration:)]) {
+ [self.debug_delegate debug_animatedImageView:self waitingForFrame:self.currentFrameIndex duration:(NSTimeInterval)displayLink.duration * displayLink.frameInterval];
+ }
+#endif
+ }
+ } else {
+ self.currentFrameIndex++;
+ }
+}
+
++ (NSString *)defaultRunLoopMode
+{
+ // Key off `activeProcessorCount` (as opposed to `processorCount`) since the system could shut down cores in certain situations.
+ return [NSProcessInfo processInfo].activeProcessorCount > 1 ? NSRunLoopCommonModes : NSDefaultRunLoopMode;
+}
+
+
+#pragma mark - CALayerDelegate (Informal)
+#pragma mark Providing the Layer's Content
+
+- (void)displayLayer:(CALayer *)layer
+{
+ layer.contents = (__bridge id)self.image.CGImage;
+}
+
+
+@end
+#pragma clang diagnostic pop
diff --git a/Apps/Wikipedia/WMF Framework/Third Party/Mantle/MTLJSONAdapter.m b/Apps/Wikipedia/WMF Framework/Third Party/Mantle/MTLJSONAdapter.m
new file mode 100644
index 0000000..965d381
--- /dev/null
+++ b/Apps/Wikipedia/WMF Framework/Third Party/Mantle/MTLJSONAdapter.m
@@ -0,0 +1,679 @@
+//
+// MTLJSONAdapter.m
+// Mantle
+//
+// Created by Justin Spahr-Summers on 2013-02-12.
+// Copyright (c) 2013 GitHub. All rights reserved.
+//
+
+#import
+
+#import "NSDictionary+MTLJSONKeyPath.h"
+
+#import "MTLEXTRuntimeExtensions.h"
+#import "MTLEXTScope.h"
+#import "MTLJSONAdapter.h"
+#import "MTLModel.h"
+#import "MTLTransformerErrorHandling.h"
+#import "MTLReflection.h"
+#import "NSValueTransformer+MTLPredefinedTransformerAdditions.h"
+#import "MTLValueTransformer.h"
+
+NSString * const MTLJSONAdapterErrorDomain = @"MTLJSONAdapterErrorDomain";
+const NSInteger MTLJSONAdapterErrorNoClassFound = 2;
+const NSInteger MTLJSONAdapterErrorInvalidJSONDictionary = 3;
+const NSInteger MTLJSONAdapterErrorInvalidJSONMapping = 4;
+
+// An exception was thrown and caught.
+const NSInteger MTLJSONAdapterErrorExceptionThrown = 1;
+
+// Associated with the NSException that was caught.
+NSString * const MTLJSONAdapterThrownExceptionErrorKey = @"MTLJSONAdapterThrownException";
+
+@interface MTLJSONAdapter ()
+
+// The MTLModel subclass being parsed, or the class of `model` if parsing has
+// completed.
+@property (nonatomic, strong, readonly) Class modelClass;
+
+// A cached copy of the return value of +JSONKeyPathsByPropertyKey.
+@property (nonatomic, copy, readonly) NSDictionary *JSONKeyPathsByPropertyKey;
+
+// A cached copy of the return value of -valueTransformersForModelClass:
+@property (nonatomic, copy, readonly) NSDictionary *valueTransformersByPropertyKey;
+
+// Used to cache the JSON adapters returned by -JSONAdapterForModelClass:error:.
+@property (nonatomic, strong, readonly) NSMapTable *JSONAdaptersByModelClass;
+
+// If +classForParsingJSONDictionary: returns a model class different from the
+// one this adapter was initialized with, use this method to obtain a cached
+// instance of a suitable adapter instead.
+//
+// modelClass - The class from which to parse the JSON. This class must conform
+// to . This argument must not be nil.
+// error - If not NULL, this may be set to an error that occurs during
+// initializing the adapter.
+//
+// Returns a JSON adapter for modelClass, creating one of necessary. If no
+// adapter could be created, nil is returned.
+- (MTLJSONAdapter *)JSONAdapterForModelClass:(Class)modelClass error:(NSError **)error;
+
+// Collect all value transformers needed for a given class.
+//
+// modelClass - The class from which to parse the JSON. This class must conform
+// to . This argument must not be nil.
+//
+// Returns a dictionary with the properties of modelClass that need
+// transformation as keys and the value transformers as values.
++ (NSDictionary *)valueTransformersForModelClass:(Class)modelClass;
+
+@end
+
+@implementation MTLJSONAdapter
+
+#pragma mark Convenience methods
+
++ (id)modelOfClass:(Class)modelClass fromJSONDictionary:(NSDictionary *)JSONDictionary error:(NSError **)error {
+ MTLJSONAdapter *adapter = [[self alloc] initWithModelClass:modelClass];
+
+ return [adapter modelFromJSONDictionary:JSONDictionary error:error];
+}
+
++ (NSArray *)modelsOfClass:(Class)modelClass fromJSONArray:(NSArray *)JSONArray error:(NSError **)error {
+ if (JSONArray == nil || ![JSONArray isKindOfClass:NSArray.class]) {
+ if (error != NULL) {
+ NSDictionary *userInfo = @{
+ NSLocalizedDescriptionKey: NSLocalizedString(@"Missing JSON array", @""),
+ NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"%@ could not be created because an invalid JSON array was provided: %@", @""), NSStringFromClass(modelClass), JSONArray.class],
+ };
+ *error = [NSError errorWithDomain:MTLJSONAdapterErrorDomain code:MTLJSONAdapterErrorInvalidJSONDictionary userInfo:userInfo];
+ }
+ return nil;
+ }
+
+ NSMutableArray *models = [NSMutableArray arrayWithCapacity:JSONArray.count];
+ for (NSDictionary *JSONDictionary in JSONArray){
+ MTLModel *model = [self modelOfClass:modelClass fromJSONDictionary:JSONDictionary error:error];
+
+ if (model == nil) return nil;
+
+ [models addObject:model];
+ }
+
+ return models;
+}
+
++ (NSDictionary *)JSONDictionaryFromModel:(id)model error:(NSError **)error {
+ MTLJSONAdapter *adapter = [[self alloc] initWithModelClass:model.class];
+
+ return [adapter JSONDictionaryFromModel:model error:error];
+}
+
++ (NSArray *)JSONArrayFromModels:(NSArray *)models error:(NSError **)error {
+ NSParameterAssert(models != nil);
+ NSParameterAssert([models isKindOfClass:NSArray.class]);
+
+ NSMutableArray *JSONArray = [NSMutableArray arrayWithCapacity:models.count];
+ for (MTLModel *model in models) {
+ NSDictionary *JSONDictionary = [self JSONDictionaryFromModel:model error:error];
+ if (JSONDictionary == nil) return nil;
+
+ [JSONArray addObject:JSONDictionary];
+ }
+
+ return JSONArray;
+}
+
+#pragma mark Lifecycle
+
+- (id)init {
+ NSAssert(NO, @"%@ must be initialized with a model class", self.class);
+ return nil;
+}
+
+- (id)initWithModelClass:(Class)modelClass {
+ NSParameterAssert(modelClass != nil);
+ NSParameterAssert([modelClass conformsToProtocol:@protocol(MTLJSONSerializing)]);
+
+ self = [super init];
+ if (self == nil) return nil;
+
+ _modelClass = modelClass;
+
+ _JSONKeyPathsByPropertyKey = [modelClass JSONKeyPathsByPropertyKey];
+
+ NSSet *propertyKeys = [self.modelClass propertyKeys];
+
+ for (NSString *mappedPropertyKey in _JSONKeyPathsByPropertyKey) {
+ if (![propertyKeys containsObject:mappedPropertyKey]) {
+ NSAssert(NO, @"%@ is not a property of %@.", mappedPropertyKey, modelClass);
+ return nil;
+ }
+
+ id value = _JSONKeyPathsByPropertyKey[mappedPropertyKey];
+
+ if ([value isKindOfClass:NSArray.class]) {
+ for (NSString *keyPath in value) {
+ if ([keyPath isKindOfClass:NSString.class]) continue;
+
+ NSAssert(NO, @"%@ must either map to a JSON key path or a JSON array of key paths, got: %@.", mappedPropertyKey, value);
+ return nil;
+ }
+ } else if (![value isKindOfClass:NSString.class]) {
+ NSAssert(NO, @"%@ must either map to a JSON key path or a JSON array of key paths, got: %@.",mappedPropertyKey, value);
+ return nil;
+ }
+ }
+
+ _valueTransformersByPropertyKey = [self.class valueTransformersForModelClass:modelClass];
+
+ _JSONAdaptersByModelClass = [NSMapTable strongToStrongObjectsMapTable];
+
+ return self;
+}
+
+#pragma mark Serialization
+
+- (NSDictionary *)JSONDictionaryFromModel:(id)model error:(NSError **)error {
+ NSParameterAssert(model != nil);
+ NSParameterAssert([model isKindOfClass:self.modelClass]);
+
+ if (self.modelClass != model.class) {
+ MTLJSONAdapter *otherAdapter = [self JSONAdapterForModelClass:model.class error:error];
+
+ return [otherAdapter JSONDictionaryFromModel:model error:error];
+ }
+
+ NSSet *propertyKeysToSerialize = [self serializablePropertyKeys:[NSSet setWithArray:self.JSONKeyPathsByPropertyKey.allKeys] forModel:model];
+
+ NSDictionary *dictionaryValue = [model.dictionaryValue dictionaryWithValuesForKeys:propertyKeysToSerialize.allObjects];
+ NSMutableDictionary *JSONDictionary = [[NSMutableDictionary alloc] initWithCapacity:dictionaryValue.count];
+
+ __block BOOL success = YES;
+ __block NSError *tmpError = nil;
+
+ [dictionaryValue enumerateKeysAndObjectsUsingBlock:^(NSString *propertyKey, id value, BOOL *stop) {
+ id JSONKeyPaths = self.JSONKeyPathsByPropertyKey[propertyKey];
+
+ if (JSONKeyPaths == nil) return;
+
+ NSValueTransformer *transformer = self.valueTransformersByPropertyKey[propertyKey];
+ if ([transformer.class allowsReverseTransformation]) {
+ // Map NSNull -> nil for the transformer, and then back for the
+ // dictionaryValue we're going to insert into.
+ if ([value isEqual:NSNull.null]) value = nil;
+
+ if ([transformer respondsToSelector:@selector(reverseTransformedValue:success:error:)]) {
+ id errorHandlingTransformer = (id)transformer;
+
+ value = [errorHandlingTransformer reverseTransformedValue:value success:&success error:&tmpError];
+
+ if (!success) {
+ *stop = YES;
+ return;
+ }
+ } else {
+ value = [transformer reverseTransformedValue:value] ?: NSNull.null;
+ }
+ }
+
+ void (^createComponents)(id, NSString *) = ^(id obj, NSString *keyPath) {
+ NSArray *keyPathComponents = [keyPath componentsSeparatedByString:@"."];
+
+ // Set up dictionaries at each step of the key path.
+ for (NSString *component in keyPathComponents) {
+ if ([obj valueForKey:component] == nil) {
+ // Insert an empty mutable dictionary at this spot so that we
+ // can set the whole key path afterward.
+ [obj setValue:[NSMutableDictionary dictionary] forKey:component];
+ }
+
+ obj = [obj valueForKey:component];
+ }
+ };
+
+ if ([JSONKeyPaths isKindOfClass:NSString.class]) {
+ createComponents(JSONDictionary, JSONKeyPaths);
+
+ [JSONDictionary setValue:value forKeyPath:JSONKeyPaths];
+ }
+
+ if ([JSONKeyPaths isKindOfClass:NSArray.class]) {
+ for (NSString *JSONKeyPath in JSONKeyPaths) {
+ createComponents(JSONDictionary, JSONKeyPath);
+
+ [JSONDictionary setValue:value[JSONKeyPath] forKeyPath:JSONKeyPath];
+ }
+ }
+ }];
+
+ if (success) {
+ return JSONDictionary;
+ } else {
+ if (error != NULL) *error = tmpError;
+
+ return nil;
+ }
+}
+
+- (id)modelFromJSONDictionary:(NSDictionary *)JSONDictionary error:(NSError **)error {
+ if ([self.modelClass respondsToSelector:@selector(classForParsingJSONDictionary:)]) {
+ Class class = [self.modelClass classForParsingJSONDictionary:JSONDictionary];
+ if (class == nil) {
+ if (error != NULL) {
+ NSDictionary *userInfo = @{
+ NSLocalizedDescriptionKey: NSLocalizedString(@"Could not parse JSON", @""),
+ NSLocalizedFailureReasonErrorKey: NSLocalizedString(@"No model class could be found to parse the JSON dictionary.", @"")
+ };
+
+ *error = [NSError errorWithDomain:MTLJSONAdapterErrorDomain code:MTLJSONAdapterErrorNoClassFound userInfo:userInfo];
+ }
+
+ return nil;
+ }
+
+ if (class != self.modelClass) {
+ NSAssert([class conformsToProtocol:@protocol(MTLJSONSerializing)], @"Class %@ returned from +classForParsingJSONDictionary: does not conform to ", class);
+
+ MTLJSONAdapter *otherAdapter = [self JSONAdapterForModelClass:class error:error];
+
+ return [otherAdapter modelFromJSONDictionary:JSONDictionary error:error];
+ }
+ }
+
+ if (JSONDictionary == nil || ![JSONDictionary isKindOfClass:NSDictionary.class]) {
+ if (error != NULL) {
+ NSDictionary *userInfo = @{
+ NSLocalizedDescriptionKey: NSLocalizedString(@"Missing JSON dictionary", @""),
+ NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"%@ could not be created because an invalid JSON dictionary was provided: %@", @""), NSStringFromClass(self.modelClass), JSONDictionary.class],
+ };
+ *error = [NSError errorWithDomain:MTLJSONAdapterErrorDomain code:MTLJSONAdapterErrorInvalidJSONDictionary userInfo:userInfo];
+ }
+ return nil;
+ }
+
+ NSMutableDictionary *dictionaryValue = [[NSMutableDictionary alloc] initWithCapacity:JSONDictionary.count];
+
+ for (NSString *propertyKey in [self.modelClass propertyKeys]) {
+ id JSONKeyPaths = self.JSONKeyPathsByPropertyKey[propertyKey];
+
+ if (JSONKeyPaths == nil) continue;
+
+ id value;
+
+ if ([JSONKeyPaths isKindOfClass:NSArray.class]) {
+ NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
+
+ for (NSString *keyPath in JSONKeyPaths) {
+ BOOL success = NO;
+ id value = [JSONDictionary mtl_valueForJSONKeyPath:keyPath success:&success error:error];
+
+ if (!success) return nil;
+
+ if (value != nil) dictionary[keyPath] = value;
+ }
+
+ value = dictionary;
+ } else {
+ BOOL success = NO;
+ value = [JSONDictionary mtl_valueForJSONKeyPath:JSONKeyPaths success:&success error:error];
+
+ if (!success) return nil;
+ }
+
+ if (value == nil) continue;
+
+ @try {
+ NSValueTransformer *transformer = self.valueTransformersByPropertyKey[propertyKey];
+ if (transformer != nil) {
+ // Map NSNull -> nil for the transformer, and then back for the
+ // dictionary we're going to insert into.
+ if ([value isEqual:NSNull.null]) value = nil;
+
+ if ([transformer respondsToSelector:@selector(transformedValue:success:error:)]) {
+ id errorHandlingTransformer = (id)transformer;
+
+ BOOL success = YES;
+ value = [errorHandlingTransformer transformedValue:value success:&success error:error];
+
+ if (!success) return nil;
+ } else {
+ value = [transformer transformedValue:value];
+ }
+
+ if (value == nil) value = NSNull.null;
+ }
+
+ dictionaryValue[propertyKey] = value;
+ } @catch (NSException *ex) {
+ NSLog(@"*** Caught exception %@ parsing JSON key path \"%@\" from: %@", ex, JSONKeyPaths, JSONDictionary);
+
+ // Fail fast in Debug builds.
+ #if DEBUG
+ @throw ex;
+ #else
+ if (error != NULL) {
+ NSDictionary *userInfo = @{
+ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Caught exception parsing JSON key path \"%@\" for model class: %@", JSONKeyPaths, self.modelClass],
+ NSLocalizedRecoverySuggestionErrorKey: ex.description,
+ NSLocalizedFailureReasonErrorKey: ex.reason,
+ MTLJSONAdapterThrownExceptionErrorKey: ex
+ };
+
+ *error = [NSError errorWithDomain:MTLJSONAdapterErrorDomain code:MTLJSONAdapterErrorExceptionThrown userInfo:userInfo];
+ }
+
+ return nil;
+ #endif
+ }
+ }
+
+ id model = [self.modelClass modelWithDictionary:dictionaryValue error:error];
+
+ return [model validate:error] ? model : nil;
+}
+
++ (NSDictionary *)valueTransformersForModelClass:(Class)modelClass {
+ NSParameterAssert(modelClass != nil);
+ NSParameterAssert([modelClass conformsToProtocol:@protocol(MTLJSONSerializing)]);
+
+ NSMutableDictionary *result = [NSMutableDictionary dictionary];
+
+ for (NSString *key in [modelClass propertyKeys]) {
+ SEL selector = MTLSelectorWithKeyPattern(key, "JSONTransformer");
+ if ([modelClass respondsToSelector:selector]) {
+ IMP imp = [modelClass methodForSelector:selector];
+ NSValueTransformer * (*function)(id, SEL) = (__typeof__(function))imp;
+ NSValueTransformer *transformer = function(modelClass, selector);
+
+ if (transformer != nil) result[key] = transformer;
+
+ continue;
+ }
+
+ if ([modelClass respondsToSelector:@selector(JSONTransformerForKey:)]) {
+ NSValueTransformer *transformer = [modelClass JSONTransformerForKey:key];
+
+ if (transformer != nil) {
+ result[key] = transformer;
+ continue;
+ }
+ }
+
+ objc_property_t property = class_getProperty(modelClass, key.UTF8String);
+
+ if (property == NULL) continue;
+
+ mtl_propertyAttributes *attributes = mtl_copyPropertyAttributes(property);
+ @onExit {
+ free(attributes);
+ };
+
+ NSValueTransformer *transformer = nil;
+
+ if (*(attributes->type) == *(@encode(id))) {
+ Class propertyClass = attributes->objectClass;
+
+ if (propertyClass != nil) {
+ transformer = [self transformerForModelPropertiesOfClass:propertyClass];
+ }
+
+
+ // For user-defined MTLModel, try parse it with dictionaryTransformer.
+ if (nil == transformer && [propertyClass conformsToProtocol:@protocol(MTLJSONSerializing)]) {
+ transformer = [self dictionaryTransformerWithModelClass:propertyClass];
+ }
+
+ if (transformer == nil) transformer = [NSValueTransformer mtl_validatingTransformerForClass:propertyClass ?: NSObject.class];
+ } else {
+ transformer = [self transformerForModelPropertiesOfObjCType:attributes->type] ?: [NSValueTransformer mtl_validatingTransformerForClass:NSValue.class];
+ }
+
+ if (transformer != nil) result[key] = transformer;
+ }
+
+ return result;
+}
+
+- (MTLJSONAdapter *)JSONAdapterForModelClass:(Class)modelClass error:(NSError **)error {
+ NSParameterAssert(modelClass != nil);
+ NSParameterAssert([modelClass conformsToProtocol:@protocol(MTLJSONSerializing)]);
+
+ @synchronized(self) {
+ MTLJSONAdapter *result = [self.JSONAdaptersByModelClass objectForKey:modelClass];
+
+ if (result != nil) return result;
+
+ result = [[self.class alloc] initWithModelClass:modelClass];
+
+ if (result != nil) {
+ [self.JSONAdaptersByModelClass setObject:result forKey:modelClass];
+ }
+
+ return result;
+ }
+}
+
+- (NSSet *)serializablePropertyKeys:(NSSet *)propertyKeys forModel:(id)model {
+ return propertyKeys;
+}
+
++ (NSValueTransformer *)transformerForModelPropertiesOfClass:(Class)modelClass {
+ NSParameterAssert(modelClass != nil);
+
+ SEL selector = MTLSelectorWithKeyPattern(NSStringFromClass(modelClass), "JSONTransformer");
+ if (![self respondsToSelector:selector]) return nil;
+
+ IMP imp = [self methodForSelector:selector];
+ NSValueTransformer * (*function)(id, SEL) = (__typeof__(function))imp;
+ NSValueTransformer *result = function(self, selector);
+
+ return result;
+}
+
++ (NSValueTransformer *)transformerForModelPropertiesOfObjCType:(const char *)objCType {
+ NSParameterAssert(objCType != NULL);
+
+ if (strcmp(objCType, @encode(BOOL)) == 0) {
+ return [NSValueTransformer valueTransformerForName:MTLBooleanValueTransformerName];
+ }
+
+ return nil;
+}
+
+@end
+
+@implementation MTLJSONAdapter (ValueTransformers)
+
++ (NSValueTransformer *)dictionaryTransformerWithModelClass:(Class)modelClass {
+ NSParameterAssert([modelClass conformsToProtocol:@protocol(MTLModel)]);
+ NSParameterAssert([modelClass conformsToProtocol:@protocol(MTLJSONSerializing)]);
+ __block MTLJSONAdapter *adapter;
+
+ return [MTLValueTransformer
+ transformerUsingForwardBlock:^ id (id JSONDictionary, BOOL *success, NSError **error) {
+ if (JSONDictionary == nil) return nil;
+
+ if (![JSONDictionary isKindOfClass:NSDictionary.class]) {
+ if (error != NULL) {
+ NSDictionary *userInfo = @{
+ NSLocalizedDescriptionKey: NSLocalizedString(@"Could not convert JSON dictionary to model object", @""),
+ NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"Expected an NSDictionary, got: %@", @""), JSONDictionary],
+ MTLTransformerErrorHandlingInputValueErrorKey : JSONDictionary
+ };
+
+ *error = [NSError errorWithDomain:MTLTransformerErrorHandlingErrorDomain code:MTLTransformerErrorHandlingErrorInvalidInput userInfo:userInfo];
+ }
+ *success = NO;
+ return nil;
+ }
+
+ if (!adapter) {
+ adapter = [[self alloc] initWithModelClass:modelClass];
+ }
+ id model = [adapter modelFromJSONDictionary:JSONDictionary error:error];
+ if (model == nil) {
+ *success = NO;
+ }
+
+ return model;
+ }
+ reverseBlock:^ NSDictionary * (id model, BOOL *success, NSError **error) {
+ if (model == nil) return nil;
+
+ if (![model conformsToProtocol:@protocol(MTLModel)] || ![model conformsToProtocol:@protocol(MTLJSONSerializing)]) {
+ if (error != NULL) {
+ NSDictionary *userInfo = @{
+ NSLocalizedDescriptionKey: NSLocalizedString(@"Could not convert model object to JSON dictionary", @""),
+ NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"Expected a MTLModel object conforming to , got: %@.", @""), model],
+ MTLTransformerErrorHandlingInputValueErrorKey : model
+ };
+
+ *error = [NSError errorWithDomain:MTLTransformerErrorHandlingErrorDomain code:MTLTransformerErrorHandlingErrorInvalidInput userInfo:userInfo];
+ }
+ *success = NO;
+ return nil;
+ }
+
+ if (!adapter) {
+ adapter = [[self alloc] initWithModelClass:modelClass];
+ }
+ NSDictionary *result = [adapter JSONDictionaryFromModel:model error:error];
+ if (result == nil) {
+ *success = NO;
+ }
+
+ return result;
+ }];
+}
+
++ (NSValueTransformer *)arrayTransformerWithModelClass:(Class)modelClass {
+ id dictionaryTransformer = [self dictionaryTransformerWithModelClass:modelClass];
+
+ return [MTLValueTransformer
+ transformerUsingForwardBlock:^ id (NSArray *dictionaries, BOOL *success, NSError **error) {
+ if (dictionaries == nil) return nil;
+
+ if (![dictionaries isKindOfClass:NSArray.class]) {
+ if (error != NULL) {
+ NSDictionary *userInfo = @{
+ NSLocalizedDescriptionKey: NSLocalizedString(@"Could not convert JSON array to model array", @""),
+ NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"Expected an NSArray, got: %@.", @""), dictionaries],
+ MTLTransformerErrorHandlingInputValueErrorKey : dictionaries
+ };
+
+ *error = [NSError errorWithDomain:MTLTransformerErrorHandlingErrorDomain code:MTLTransformerErrorHandlingErrorInvalidInput userInfo:userInfo];
+ }
+ *success = NO;
+ return nil;
+ }
+
+ NSMutableArray *models = [NSMutableArray arrayWithCapacity:dictionaries.count];
+ for (id JSONDictionary in dictionaries) {
+ if (JSONDictionary == NSNull.null) {
+ [models addObject:NSNull.null];
+ continue;
+ }
+
+ if (![JSONDictionary isKindOfClass:NSDictionary.class]) {
+ if (error != NULL) {
+ NSDictionary *userInfo = @{
+ NSLocalizedDescriptionKey: NSLocalizedString(@"Could not convert JSON array to model array", @""),
+ NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"Expected an NSDictionary or an NSNull, got: %@.", @""), JSONDictionary],
+ MTLTransformerErrorHandlingInputValueErrorKey : JSONDictionary
+ };
+
+ *error = [NSError errorWithDomain:MTLTransformerErrorHandlingErrorDomain code:MTLTransformerErrorHandlingErrorInvalidInput userInfo:userInfo];
+ }
+ *success = NO;
+ return nil;
+ }
+
+ id model = [dictionaryTransformer transformedValue:JSONDictionary success:success error:error];
+
+ if (*success == NO) return nil;
+
+ if (model == nil) continue;
+
+ [models addObject:model];
+ }
+
+ return models;
+ }
+ reverseBlock:^ id (NSArray *models, BOOL *success, NSError **error) {
+ if (models == nil) return nil;
+
+ if (![models isKindOfClass:NSArray.class]) {
+ if (error != NULL) {
+ NSDictionary *userInfo = @{
+ NSLocalizedDescriptionKey: NSLocalizedString(@"Could not convert model array to JSON array", @""),
+ NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"Expected an NSArray, got: %@.", @""), models],
+ MTLTransformerErrorHandlingInputValueErrorKey : models
+ };
+
+ *error = [NSError errorWithDomain:MTLTransformerErrorHandlingErrorDomain code:MTLTransformerErrorHandlingErrorInvalidInput userInfo:userInfo];
+ }
+ *success = NO;
+ return nil;
+ }
+
+ NSMutableArray *dictionaries = [NSMutableArray arrayWithCapacity:models.count];
+ for (id model in models) {
+ if (model == NSNull.null) {
+ [dictionaries addObject:NSNull.null];
+ continue;
+ }
+
+ if (![model isKindOfClass:MTLModel.class]) {
+ if (error != NULL) {
+ NSDictionary *userInfo = @{
+ NSLocalizedDescriptionKey: NSLocalizedString(@"Could not convert JSON array to model array", @""),
+ NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"Expected a MTLModel or an NSNull, got: %@.", @""), model],
+ MTLTransformerErrorHandlingInputValueErrorKey : model
+ };
+
+ *error = [NSError errorWithDomain:MTLTransformerErrorHandlingErrorDomain code:MTLTransformerErrorHandlingErrorInvalidInput userInfo:userInfo];
+ }
+ *success = NO;
+ return nil;
+ }
+
+ NSDictionary *dict = [dictionaryTransformer reverseTransformedValue:model success:success error:error];
+
+ if (*success == NO) return nil;
+
+ if (dict == nil) continue;
+
+ [dictionaries addObject:dict];
+ }
+
+ return dictionaries;
+ }];
+}
+
++ (NSValueTransformer *)NSURLJSONTransformer {
+ return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];
+}
+
++ (NSValueTransformer *)NSUUIDJSONTransformer {
+ return [NSValueTransformer valueTransformerForName:MTLUUIDValueTransformerName];
+}
+
+@end
+
+@implementation MTLJSONAdapter (Deprecated)
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-implementations"
+
++ (NSArray *)JSONArrayFromModels:(NSArray *)models {
+ return [self JSONArrayFromModels:models error:NULL];
+}
+
++ (NSDictionary *)JSONDictionaryFromModel:(MTLModel *)model {
+ return [self JSONDictionaryFromModel:model error:NULL];
+}
+
+#pragma clang diagnostic pop
+
+@end
diff --git a/Apps/Wikipedia/WMF Framework/Third Party/Mantle/MTLModel+NSCoding.m b/Apps/Wikipedia/WMF Framework/Third Party/Mantle/MTLModel+NSCoding.m
new file mode 100644
index 0000000..51358db
--- /dev/null
+++ b/Apps/Wikipedia/WMF Framework/Third Party/Mantle/MTLModel+NSCoding.m
@@ -0,0 +1,261 @@
+//
+// MTLModel+NSCoding.m
+// Mantle
+//
+// Created by Justin Spahr-Summers on 2013-02-12.
+// Copyright (c) 2013 GitHub. All rights reserved.
+//
+
+#import "MTLEXTRuntimeExtensions.h"
+#import "MTLEXTScope.h"
+#import "MTLModel+NSCoding.h"
+#import "MTLReflection.h"
+
+// Used in archives to store the modelVersion of the archived instance.
+static NSString * const MTLModelVersionKey = @"MTLModelVersion";
+
+// Used to cache the reflection performed in +allowedSecureCodingClassesByPropertyKey.
+static void *MTLModelCachedAllowedClassesKey = &MTLModelCachedAllowedClassesKey;
+
+// Returns whether the given NSCoder requires secure coding.
+static BOOL coderRequiresSecureCoding(NSCoder *coder) {
+ SEL requiresSecureCodingSelector = @selector(requiresSecureCoding);
+
+ // Only invoke the method if it's implemented (i.e., only on OS X 10.8+ and
+ // iOS 6+).
+ if (![coder respondsToSelector:requiresSecureCodingSelector]) return NO;
+
+ BOOL (*requiresSecureCodingIMP)(NSCoder *, SEL) = (__typeof__(requiresSecureCodingIMP))[coder methodForSelector:requiresSecureCodingSelector];
+ if (requiresSecureCodingIMP == NULL) return NO;
+
+ return requiresSecureCodingIMP(coder, requiresSecureCodingSelector);
+}
+
+// Returns all of the given class' encodable property keys (those that will not
+// be excluded from archives).
+static NSSet *encodablePropertyKeysForClass(Class modelClass) {
+ return [[modelClass encodingBehaviorsByPropertyKey] keysOfEntriesPassingTest:^ BOOL (NSString *propertyKey, NSNumber *behavior, BOOL *stop) {
+ return behavior.unsignedIntegerValue != MTLModelEncodingBehaviorExcluded;
+ }];
+}
+
+// Verifies that all of the specified class' encodable property keys are present
+// in +allowedSecureCodingClassesByPropertyKey, and throws an exception if not.
+static void verifyAllowedClassesByPropertyKey(Class modelClass) {
+ NSDictionary *allowedClasses = [modelClass allowedSecureCodingClassesByPropertyKey];
+
+ NSMutableSet *specifiedPropertyKeys = [[NSMutableSet alloc] initWithArray:allowedClasses.allKeys];
+ [specifiedPropertyKeys minusSet:encodablePropertyKeysForClass(modelClass)];
+
+ if (specifiedPropertyKeys.count > 0) {
+ [NSException raise:NSInvalidArgumentException format:@"Cannot encode %@ securely, because keys are missing from +allowedSecureCodingClassesByPropertyKey: %@", modelClass, specifiedPropertyKeys];
+ }
+}
+
+@implementation MTLModel (NSCoding)
+
+#pragma mark Versioning
+
++ (NSUInteger)modelVersion {
+ return 0;
+}
+
+#pragma mark Encoding Behaviors
+
++ (NSDictionary *)encodingBehaviorsByPropertyKey {
+ NSSet *propertyKeys = self.propertyKeys;
+ NSMutableDictionary *behaviors = [[NSMutableDictionary alloc] initWithCapacity:propertyKeys.count];
+
+ for (NSString *key in propertyKeys) {
+ objc_property_t property = class_getProperty(self, key.UTF8String);
+ NSAssert(property != NULL, @"Could not find property \"%@\" on %@", key, self);
+
+ mtl_propertyAttributes *attributes = mtl_copyPropertyAttributes(property);
+ @onExit {
+ free(attributes);
+ };
+
+ MTLModelEncodingBehavior behavior = (attributes->weak ? MTLModelEncodingBehaviorConditional : MTLModelEncodingBehaviorUnconditional);
+ behaviors[key] = @(behavior);
+ }
+
+ return behaviors;
+}
+
++ (NSDictionary *)allowedSecureCodingClassesByPropertyKey {
+ NSDictionary *cachedClasses = objc_getAssociatedObject(self, MTLModelCachedAllowedClassesKey);
+ if (cachedClasses != nil) return cachedClasses;
+
+ // Get all property keys that could potentially be encoded.
+ NSSet *propertyKeys = [self.encodingBehaviorsByPropertyKey keysOfEntriesPassingTest:^ BOOL (NSString *propertyKey, NSNumber *behavior, BOOL *stop) {
+ return behavior.unsignedIntegerValue != MTLModelEncodingBehaviorExcluded;
+ }];
+
+ NSMutableDictionary *allowedClasses = [[NSMutableDictionary alloc] initWithCapacity:propertyKeys.count];
+
+ for (NSString *key in propertyKeys) {
+ objc_property_t property = class_getProperty(self, key.UTF8String);
+ NSAssert(property != NULL, @"Could not find property \"%@\" on %@", key, self);
+
+ mtl_propertyAttributes *attributes = mtl_copyPropertyAttributes(property);
+ @onExit {
+ free(attributes);
+ };
+
+ // If the property is not of object or class type, assume that it's
+ // a primitive which would be boxed into an NSValue.
+ if (attributes->type[0] != '@' && attributes->type[0] != '#') {
+ allowedClasses[key] = @[ NSValue.class ];
+ continue;
+ }
+
+ // Omit this property from the dictionary if its class isn't known.
+ if (attributes->objectClass != nil) {
+ allowedClasses[key] = @[ attributes->objectClass ];
+ }
+ }
+
+ // It doesn't really matter if we replace another thread's work, since we do
+ // it atomically and the result should be the same.
+ objc_setAssociatedObject(self, MTLModelCachedAllowedClassesKey, allowedClasses, OBJC_ASSOCIATION_COPY);
+
+ return allowedClasses;
+}
+
+- (id)decodeValueForKey:(NSString *)key withCoder:(NSCoder *)coder modelVersion:(NSUInteger)modelVersion {
+ NSParameterAssert(key != nil);
+ NSParameterAssert(coder != nil);
+
+ SEL selector = MTLSelectorWithCapitalizedKeyPattern("decode", key, "WithCoder:modelVersion:");
+ if ([self respondsToSelector:selector]) {
+ IMP imp = [self methodForSelector:selector];
+ id (*function)(id, SEL, NSCoder *, NSUInteger) = (__typeof__(function))imp;
+ id result = function(self, selector, coder, modelVersion);
+
+ return result;
+ }
+
+ @try {
+ if (coderRequiresSecureCoding(coder)) {
+ NSArray *allowedClasses = self.class.allowedSecureCodingClassesByPropertyKey[key];
+ NSAssert(allowedClasses != nil, @"No allowed classes specified for securely decoding key \"%@\" on %@", key, self.class);
+
+ return [coder decodeObjectOfClasses:[NSSet setWithArray:allowedClasses] forKey:key];
+ } else {
+ return [coder decodeObjectForKey:key];
+ }
+ } @catch (NSException *ex) {
+ NSLog(@"*** Caught exception decoding value for key \"%@\" on class %@: %@", key, self.class, ex);
+ @throw ex;
+ }
+}
+
+#pragma mark NSCoding
+
+- (instancetype)initWithCoder:(NSCoder *)coder {
+ BOOL requiresSecureCoding = coderRequiresSecureCoding(coder);
+ NSNumber *version = nil;
+ if (requiresSecureCoding) {
+ version = [coder decodeObjectOfClass:NSNumber.class forKey:MTLModelVersionKey];
+ } else {
+ version = [coder decodeObjectForKey:MTLModelVersionKey];
+ }
+
+ if (version == nil) {
+ NSLog(@"Warning: decoding an archive of %@ without a version, assuming 0", self.class);
+ } else if (version.unsignedIntegerValue > self.class.modelVersion) {
+ // Don't try to decode newer versions.
+ return nil;
+ }
+
+ if (requiresSecureCoding) {
+ verifyAllowedClassesByPropertyKey(self.class);
+ } else {
+ // Handle the old archive format.
+ NSDictionary *externalRepresentation = [coder decodeObjectForKey:@"externalRepresentation"];
+ if (externalRepresentation != nil) {
+ NSAssert([self.class methodForSelector:@selector(dictionaryValueFromArchivedExternalRepresentation:version:)] != [MTLModel methodForSelector:@selector(dictionaryValueFromArchivedExternalRepresentation:version:)], @"Decoded an old archive of %@ that contains an externalRepresentation, but +dictionaryValueFromArchivedExternalRepresentation:version: is not overridden to handle it", self.class);
+
+ NSDictionary *dictionaryValue = [self.class dictionaryValueFromArchivedExternalRepresentation:externalRepresentation version:version.unsignedIntegerValue];
+ if (dictionaryValue == nil) return nil;
+
+ NSError *error = nil;
+ self = [self initWithDictionary:dictionaryValue error:&error];
+ if (self == nil) NSLog(@"*** Could not decode old %@ archive: %@", self.class, error);
+
+ return self;
+ }
+ }
+
+ NSSet *propertyKeys = self.class.propertyKeys;
+ NSMutableDictionary *dictionaryValue = [[NSMutableDictionary alloc] initWithCapacity:propertyKeys.count];
+
+ for (NSString *key in propertyKeys) {
+ id value = [self decodeValueForKey:key withCoder:coder modelVersion:version.unsignedIntegerValue];
+ if (value == nil) continue;
+
+ dictionaryValue[key] = value;
+ }
+
+ NSError *error = nil;
+ self = [self initWithDictionary:dictionaryValue error:&error];
+ if (self == nil) NSLog(@"*** Could not unarchive %@: %@", self.class, error);
+
+ return self;
+}
+
+- (void)encodeWithCoder:(NSCoder *)coder {
+ if (coderRequiresSecureCoding(coder)) verifyAllowedClassesByPropertyKey(self.class);
+
+ [coder encodeObject:@(self.class.modelVersion) forKey:MTLModelVersionKey];
+
+ NSDictionary *encodingBehaviors = self.class.encodingBehaviorsByPropertyKey;
+ [self.dictionaryValue enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) {
+ @try {
+ // Skip nil values.
+ if ([value isEqual:NSNull.null]) return;
+
+ switch ([encodingBehaviors[key] unsignedIntegerValue]) {
+ // This will also match a nil behavior.
+ case MTLModelEncodingBehaviorExcluded:
+ break;
+
+ case MTLModelEncodingBehaviorUnconditional:
+ [coder encodeObject:value forKey:key];
+ break;
+
+ case MTLModelEncodingBehaviorConditional:
+ [coder encodeConditionalObject:value forKey:key];
+ break;
+
+ default:
+ NSAssert(NO, @"Unrecognized encoding behavior %@ on class %@ for key \"%@\"", self.class, encodingBehaviors[key], key);
+ }
+ } @catch (NSException *ex) {
+ NSLog(@"*** Caught exception encoding value for key \"%@\" on class %@: %@", key, self.class, ex);
+ @throw ex;
+ }
+ }];
+}
+
+#pragma mark NSSecureCoding
+
++ (BOOL)supportsSecureCoding {
+ // Disable secure coding support by default, so subclasses are forced to
+ // opt-in by conforming to the protocol and overriding this method.
+ //
+ // We only implement this method because XPC complains if a subclass tries
+ // to implement it but does not override -initWithCoder:. See
+ // https://github.com/github/Mantle/issues/74.
+ return NO;
+}
+
+@end
+
+@implementation MTLModel (OldArchiveSupport)
+
++ (NSDictionary *)dictionaryValueFromArchivedExternalRepresentation:(NSDictionary *)externalRepresentation version:(NSUInteger)fromVersion {
+ return nil;
+}
+
+@end
diff --git a/Apps/Wikipedia/WMF Framework/Third Party/Mantle/MTLModel.m b/Apps/Wikipedia/WMF Framework/Third Party/Mantle/MTLModel.m
new file mode 100644
index 0000000..b3b756b
--- /dev/null
+++ b/Apps/Wikipedia/WMF Framework/Third Party/Mantle/MTLModel.m
@@ -0,0 +1,334 @@
+//
+// MTLModel.m
+// Mantle
+//
+// Created by Justin Spahr-Summers on 2012-09-11.
+// Copyright (c) 2012 GitHub. All rights reserved.
+//
+
+#import "MTLEXTRuntimeExtensions.h"
+#import "MTLEXTScope.h"
+#import "MTLModel.h"
+#import "MTLReflection.h"
+#import "NSError+MTLModelException.h"
+#import
+
+// Used to cache the reflection performed in +propertyKeys.
+static void *MTLModelCachedPropertyKeysKey = &MTLModelCachedPropertyKeysKey;
+
+// Associated in +generateAndCachePropertyKeys with a set of all transitory
+// property keys.
+static void *MTLModelCachedTransitoryPropertyKeysKey = &MTLModelCachedTransitoryPropertyKeysKey;
+
+// Associated in +generateAndCachePropertyKeys with a set of all permanent
+// property keys.
+static void *MTLModelCachedPermanentPropertyKeysKey = &MTLModelCachedPermanentPropertyKeysKey;
+
+// Validates a value for an object and sets it if necessary.
+//
+// obj - The object for which the value is being validated. This value
+// must not be nil.
+// key - The name of one of `obj`s properties. This value must not be
+// nil.
+// value - The new value for the property identified by `key`.
+// forceUpdate - If set to `YES`, the value is being updated even if validating
+// it did not change it.
+// error - If not NULL, this may be set to any error that occurs during
+// validation
+//
+// Returns YES if `value` could be validated and set, or NO if an error
+// occurred.
+static BOOL MTLValidateAndSetValue(id obj, NSString *key, id value, BOOL forceUpdate, NSError **error) {
+ // Mark this as being autoreleased, because validateValue may return
+ // a new object to be stored in this variable (and we don't want ARC to
+ // double-free or leak the old or new values).
+ __autoreleasing id validatedValue = value;
+
+ @try {
+ if (![obj validateValue:&validatedValue forKey:key error:error]) return NO;
+
+ if (forceUpdate || value != validatedValue) {
+ [obj setValue:validatedValue forKey:key];
+ }
+
+ return YES;
+ } @catch (NSException *ex) {
+ NSLog(@"*** Caught exception setting key \"%@\" : %@", key, ex);
+
+ // Fail fast in Debug builds.
+ #if DEBUG
+ @throw ex;
+ #else
+ if (error != NULL) {
+ *error = [NSError mtl_modelErrorWithException:ex];
+ }
+
+ return NO;
+ #endif
+ }
+}
+
+@interface MTLModel ()
+
+// Inspects all properties of returned by +propertyKeys using
+// +storageBehaviorForPropertyWithKey and caches the results.
++ (void)generateAndCacheStorageBehaviors;
+
+// Returns a set of all property keys for which
+// +storageBehaviorForPropertyWithKey returned MTLPropertyStorageTransitory.
++ (NSSet *)transitoryPropertyKeys;
+
+// Returns a set of all property keys for which
+// +storageBehaviorForPropertyWithKey returned MTLPropertyStoragePermanent.
++ (NSSet *)permanentPropertyKeys;
+
+// Enumerates all properties of the receiver's class hierarchy, starting at the
+// receiver, and continuing up until (but not including) MTLModel.
+//
+// The given block will be invoked multiple times for any properties declared on
+// multiple classes in the hierarchy.
++ (void)enumeratePropertiesUsingBlock:(void (^)(objc_property_t property, BOOL *stop))block;
+
+@end
+
+@implementation MTLModel
+
+#pragma mark Lifecycle
+
++ (void)generateAndCacheStorageBehaviors {
+ NSMutableSet *transitoryKeys = [NSMutableSet set];
+ NSMutableSet *permanentKeys = [NSMutableSet set];
+
+ for (NSString *propertyKey in self.propertyKeys) {
+ switch ([self storageBehaviorForPropertyWithKey:propertyKey]) {
+ case MTLPropertyStorageNone:
+ break;
+
+ case MTLPropertyStorageTransitory:
+ [transitoryKeys addObject:propertyKey];
+ break;
+
+ case MTLPropertyStoragePermanent:
+ [permanentKeys addObject:propertyKey];
+ break;
+ }
+ }
+
+ // It doesn't really matter if we replace another thread's work, since we do
+ // it atomically and the result should be the same.
+ objc_setAssociatedObject(self, MTLModelCachedTransitoryPropertyKeysKey, transitoryKeys, OBJC_ASSOCIATION_COPY);
+ objc_setAssociatedObject(self, MTLModelCachedPermanentPropertyKeysKey, permanentKeys, OBJC_ASSOCIATION_COPY);
+}
+
++ (instancetype)modelWithDictionary:(NSDictionary *)dictionary error:(NSError **)error {
+ return [[self alloc] initWithDictionary:dictionary error:error];
+}
+
+- (instancetype)init {
+ // Nothing special by default, but we have a declaration in the header.
+ return [super init];
+}
+
+- (instancetype)initWithDictionary:(NSDictionary *)dictionary error:(NSError **)error {
+ self = [self init];
+ if (self == nil) return nil;
+
+ for (NSString *key in dictionary) {
+ // Mark this as being autoreleased, because validateValue may return
+ // a new object to be stored in this variable (and we don't want ARC to
+ // double-free or leak the old or new values).
+ __autoreleasing id value = [dictionary objectForKey:key];
+
+ if ([value isEqual:NSNull.null]) value = nil;
+
+ BOOL success = MTLValidateAndSetValue(self, key, value, YES, error);
+ if (!success) return nil;
+ }
+
+ return self;
+}
+
+#pragma mark Reflection
+
++ (void)enumeratePropertiesUsingBlock:(void (^)(objc_property_t property, BOOL *stop))block {
+ Class cls = self;
+ BOOL stop = NO;
+
+ while (!stop && ![cls isEqual:MTLModel.class]) {
+ unsigned count = 0;
+ objc_property_t *properties = class_copyPropertyList(cls, &count);
+
+ cls = cls.superclass;
+ if (properties == NULL) continue;
+
+ @onExit {
+ free(properties);
+ };
+
+ for (unsigned i = 0; i < count; i++) {
+ block(properties[i], &stop);
+ if (stop) break;
+ }
+ }
+}
+
++ (NSSet *)propertyKeys {
+ NSSet *cachedKeys = objc_getAssociatedObject(self, MTLModelCachedPropertyKeysKey);
+ if (cachedKeys != nil) return cachedKeys;
+
+ NSMutableSet *keys = [NSMutableSet set];
+
+ [self enumeratePropertiesUsingBlock:^(objc_property_t property, BOOL *stop) {
+ NSString *key = @(property_getName(property));
+
+ if ([self storageBehaviorForPropertyWithKey:key] != MTLPropertyStorageNone) {
+ [keys addObject:key];
+ }
+ }];
+
+ // It doesn't really matter if we replace another thread's work, since we do
+ // it atomically and the result should be the same.
+ objc_setAssociatedObject(self, MTLModelCachedPropertyKeysKey, keys, OBJC_ASSOCIATION_COPY);
+
+ return keys;
+}
+
++ (NSSet *)transitoryPropertyKeys {
+ NSSet *transitoryPropertyKeys = objc_getAssociatedObject(self, MTLModelCachedTransitoryPropertyKeysKey);
+
+ if (transitoryPropertyKeys == nil) {
+ [self generateAndCacheStorageBehaviors];
+ transitoryPropertyKeys = objc_getAssociatedObject(self, MTLModelCachedTransitoryPropertyKeysKey);
+ }
+
+ return transitoryPropertyKeys;
+}
+
++ (NSSet *)permanentPropertyKeys {
+ NSSet *permanentPropertyKeys = objc_getAssociatedObject(self, MTLModelCachedPermanentPropertyKeysKey);
+
+ if (permanentPropertyKeys == nil) {
+ [self generateAndCacheStorageBehaviors];
+ permanentPropertyKeys = objc_getAssociatedObject(self, MTLModelCachedPermanentPropertyKeysKey);
+ }
+
+ return permanentPropertyKeys;
+}
+
+- (NSDictionary *)dictionaryValue {
+ NSSet *keys = [self.class.transitoryPropertyKeys setByAddingObjectsFromSet:self.class.permanentPropertyKeys];
+
+ return [self dictionaryWithValuesForKeys:keys.allObjects];
+}
+
++ (MTLPropertyStorage)storageBehaviorForPropertyWithKey:(NSString *)propertyKey {
+ objc_property_t property = class_getProperty(self.class, propertyKey.UTF8String);
+
+ if (property == NULL) return MTLPropertyStorageNone;
+
+ mtl_propertyAttributes *attributes = mtl_copyPropertyAttributes(property);
+ @onExit {
+ free(attributes);
+ };
+
+ BOOL hasGetter = [self instancesRespondToSelector:attributes->getter];
+ BOOL hasSetter = [self instancesRespondToSelector:attributes->setter];
+ if (!attributes->dynamic && attributes->ivar == NULL && !hasGetter && !hasSetter) {
+ return MTLPropertyStorageNone;
+ } else if (attributes->readonly && attributes->ivar == NULL) {
+ if ([self isEqual:MTLModel.class]) {
+ return MTLPropertyStorageNone;
+ } else {
+ // Check superclass in case the subclass redeclares a property that
+ // falls through
+ return [self.superclass storageBehaviorForPropertyWithKey:propertyKey];
+ }
+ } else {
+ return MTLPropertyStoragePermanent;
+ }
+}
+
+#pragma mark Merging
+
+- (void)mergeValueForKey:(NSString *)key fromModel:(NSObject *)model {
+ NSParameterAssert(key != nil);
+
+ SEL selector = MTLSelectorWithCapitalizedKeyPattern("merge", key, "FromModel:");
+ if (![self respondsToSelector:selector]) {
+ if (model != nil) {
+ [self setValue:[model valueForKey:key] forKey:key];
+ }
+
+ return;
+ }
+
+ IMP imp = [self methodForSelector:selector];
+ void (*function)(id, SEL, id) = (__typeof__(function))imp;
+ function(self, selector, model);
+}
+
+- (void)mergeValuesForKeysFromModel:(id)model {
+ NSSet *propertyKeys = model.class.propertyKeys;
+
+ for (NSString *key in self.class.propertyKeys) {
+ if (![propertyKeys containsObject:key]) continue;
+
+ [self mergeValueForKey:key fromModel:model];
+ }
+}
+
+#pragma mark Validation
+
+- (BOOL)validate:(NSError **)error {
+ for (NSString *key in self.class.propertyKeys) {
+ id value = [self valueForKey:key];
+
+ BOOL success = MTLValidateAndSetValue(self, key, value, NO, error);
+ if (!success) return NO;
+ }
+
+ return YES;
+}
+
+#pragma mark NSCopying
+
+- (instancetype)copyWithZone:(NSZone *)zone {
+ MTLModel *copy = [[self.class allocWithZone:zone] init];
+ [copy setValuesForKeysWithDictionary:self.dictionaryValue];
+ return copy;
+}
+
+#pragma mark NSObject
+
+- (NSString *)description {
+ NSDictionary *permanentProperties = [self dictionaryWithValuesForKeys:self.class.permanentPropertyKeys.allObjects];
+
+ return [NSString stringWithFormat:@"<%@: %p> %@", self.class, self, permanentProperties];
+}
+
+- (NSUInteger)hash {
+ NSUInteger value = 0;
+
+ for (NSString *key in self.class.permanentPropertyKeys) {
+ value ^= [[self valueForKey:key] hash];
+ }
+
+ return value;
+}
+
+- (BOOL)isEqual:(MTLModel *)model {
+ if (self == model) return YES;
+ if (![model isMemberOfClass:self.class]) return NO;
+
+ for (NSString *key in self.class.permanentPropertyKeys) {
+ id selfValue = [self valueForKey:key];
+ id modelValue = [model valueForKey:key];
+
+ BOOL valuesEqual = ((selfValue == nil && modelValue == nil) || [selfValue isEqual:modelValue]);
+ if (!valuesEqual) return NO;
+ }
+
+ return YES;
+}
+
+@end
diff --git a/Apps/Wikipedia/WMF Framework/Third Party/Mantle/MTLReflection.h b/Apps/Wikipedia/WMF Framework/Third Party/Mantle/MTLReflection.h
new file mode 100644
index 0000000..52c920b
--- /dev/null
+++ b/Apps/Wikipedia/WMF Framework/Third Party/Mantle/MTLReflection.h
@@ -0,0 +1,31 @@
+//
+// MTLReflection.h
+// Mantle
+//
+// Created by Justin Spahr-Summers on 2013-03-12.
+// Copyright (c) 2013 GitHub. All rights reserved.
+//
+
+#import
+
+/// Creates a selector from a key and a constant string.
+///
+/// key - The key to insert into the generated selector. This key should be in
+/// its natural case.
+/// suffix - A string to append to the key as part of the selector.
+///
+/// Returns a selector, or NULL if the input strings cannot form a valid
+/// selector.
+SEL MTLSelectorWithKeyPattern(NSString *key, const char *suffix) __attribute__((pure, nonnull(1, 2)));
+
+/// Creates a selector from a key and a constant prefix and suffix.
+///
+/// prefix - A string to prepend to the key as part of the selector.
+/// key - The key to insert into the generated selector. This key should be in
+/// its natural case, and will have its first letter capitalized when
+/// inserted.
+/// suffix - A string to append to the key as part of the selector.
+///
+/// Returns a selector, or NULL if the input strings cannot form a valid
+/// selector.
+SEL MTLSelectorWithCapitalizedKeyPattern(const char *prefix, NSString *key, const char *suffix) __attribute__((pure, nonnull(1, 2, 3)));
diff --git a/Apps/Wikipedia/WMF Framework/Third Party/Mantle/MTLReflection.m b/Apps/Wikipedia/WMF Framework/Third Party/Mantle/MTLReflection.m
new file mode 100644
index 0000000..923e9e2
--- /dev/null
+++ b/Apps/Wikipedia/WMF Framework/Third Party/Mantle/MTLReflection.m
@@ -0,0 +1,50 @@
+//
+// MTLReflection.m
+// Mantle
+//
+// Created by Justin Spahr-Summers on 2013-03-12.
+// Copyright (c) 2013 GitHub. All rights reserved.
+//
+
+#import "MTLReflection.h"
+#import
+
+SEL MTLSelectorWithKeyPattern(NSString *key, const char *suffix) {
+ NSUInteger keyLength = [key maximumLengthOfBytesUsingEncoding:NSUTF8StringEncoding];
+ NSUInteger suffixLength = strlen(suffix);
+
+ char selector[keyLength + suffixLength + 1];
+
+ BOOL success = [key getBytes:selector maxLength:keyLength usedLength:&keyLength encoding:NSUTF8StringEncoding options:0 range:NSMakeRange(0, key.length) remainingRange:NULL];
+ if (!success) return NULL;
+
+ memcpy(selector + keyLength, suffix, suffixLength);
+ selector[keyLength + suffixLength] = '\0';
+
+ return sel_registerName(selector);
+}
+
+SEL MTLSelectorWithCapitalizedKeyPattern(const char *prefix, NSString *key, const char *suffix) {
+ NSUInteger prefixLength = strlen(prefix);
+ NSUInteger suffixLength = strlen(suffix);
+
+ NSString *initial = [key substringToIndex:1].uppercaseString;
+ NSUInteger initialLength = [initial maximumLengthOfBytesUsingEncoding:NSUTF8StringEncoding];
+
+ NSString *rest = [key substringFromIndex:1];
+ NSUInteger restLength = [rest maximumLengthOfBytesUsingEncoding:NSUTF8StringEncoding];
+
+ char selector[prefixLength + initialLength + restLength + suffixLength + 1];
+ memcpy(selector, prefix, prefixLength);
+
+ BOOL success = [initial getBytes:selector + prefixLength maxLength:initialLength usedLength:&initialLength encoding:NSUTF8StringEncoding options:0 range:NSMakeRange(0, initial.length) remainingRange:NULL];
+ if (!success) return NULL;
+
+ success = [rest getBytes:selector + prefixLength + initialLength maxLength:restLength usedLength:&restLength encoding:NSUTF8StringEncoding options:0 range:NSMakeRange(0, rest.length) remainingRange:NULL];
+ if (!success) return NULL;
+
+ memcpy(selector + prefixLength + initialLength + restLength, suffix, suffixLength);
+ selector[prefixLength + initialLength + restLength + suffixLength] = '\0';
+
+ return sel_registerName(selector);
+}
diff --git a/Apps/Wikipedia/WMF Framework/Third Party/Mantle/MTLTransformerErrorHandling.m b/Apps/Wikipedia/WMF Framework/Third Party/Mantle/MTLTransformerErrorHandling.m
new file mode 100644
index 0000000..962e2c0
--- /dev/null
+++ b/Apps/Wikipedia/WMF Framework/Third Party/Mantle/MTLTransformerErrorHandling.m
@@ -0,0 +1,15 @@
+//
+// MTLTransformerErrorHandling.h
+// Mantle
+//
+// Created by Robert Böhnke on 10/6/13.
+// Copyright (c) 2013 GitHub. All rights reserved.
+//
+
+#import "MTLTransformerErrorHandling.h"
+
+NSString * const MTLTransformerErrorHandlingErrorDomain = @"MTLTransformerErrorHandlingErrorDomain";
+
+const NSInteger MTLTransformerErrorHandlingErrorInvalidInput = 1;
+
+NSString * const MTLTransformerErrorHandlingInputValueErrorKey = @"MTLTransformerErrorHandlingInputValueErrorKey";
diff --git a/Apps/Wikipedia/WMF Framework/Third Party/Mantle/MTLValueTransformer.m b/Apps/Wikipedia/WMF Framework/Third Party/Mantle/MTLValueTransformer.m
new file mode 100644
index 0000000..701627b
--- /dev/null
+++ b/Apps/Wikipedia/WMF Framework/Third Party/Mantle/MTLValueTransformer.m
@@ -0,0 +1,150 @@
+//
+// MTLValueTransformer.m
+// Mantle
+//
+// Created by Justin Spahr-Summers on 2012-09-11.
+// Copyright (c) 2012 GitHub. All rights reserved.
+//
+
+#import "MTLValueTransformer.h"
+
+//
+// Any MTLValueTransformer supporting reverse transformation. Necessary because
+// +allowsReverseTransformation is a class method.
+//
+@interface MTLReversibleValueTransformer : MTLValueTransformer
+@end
+
+@interface MTLValueTransformer ()
+
+@property (nonatomic, copy, readonly) MTLValueTransformerBlock forwardBlock;
+@property (nonatomic, copy, readonly) MTLValueTransformerBlock reverseBlock;
+
+@end
+
+@implementation MTLValueTransformer
+
+#pragma mark Lifecycle
+
++ (instancetype)transformerUsingForwardBlock:(MTLValueTransformerBlock)forwardBlock {
+ return [[self alloc] initWithForwardBlock:forwardBlock reverseBlock:nil];
+}
+
++ (instancetype)transformerUsingReversibleBlock:(MTLValueTransformerBlock)reversibleBlock {
+ return [self transformerUsingForwardBlock:reversibleBlock reverseBlock:reversibleBlock];
+}
+
++ (instancetype)transformerUsingForwardBlock:(MTLValueTransformerBlock)forwardBlock reverseBlock:(MTLValueTransformerBlock)reverseBlock {
+ return [[MTLReversibleValueTransformer alloc] initWithForwardBlock:forwardBlock reverseBlock:reverseBlock];
+}
+
+- (id)initWithForwardBlock:(MTLValueTransformerBlock)forwardBlock reverseBlock:(MTLValueTransformerBlock)reverseBlock {
+ NSParameterAssert(forwardBlock != nil);
+
+ self = [super init];
+ if (self == nil) return nil;
+
+ _forwardBlock = [forwardBlock copy];
+ _reverseBlock = [reverseBlock copy];
+
+ return self;
+}
+
+#pragma mark NSValueTransformer
+
++ (BOOL)allowsReverseTransformation {
+ return NO;
+}
+
++ (Class)transformedValueClass {
+ return NSObject.class;
+}
+
+- (id)transformedValue:(id)value {
+ NSError *error = nil;
+ BOOL success = YES;
+
+ return self.forwardBlock(value, &success, &error);
+}
+
+- (id)transformedValue:(id)value success:(BOOL *)outerSuccess error:(NSError **)outerError {
+ NSError *error = nil;
+ BOOL success = YES;
+
+ id transformedValue = self.forwardBlock(value, &success, &error);
+
+ if (outerSuccess != NULL) *outerSuccess = success;
+ if (outerError != NULL) *outerError = error;
+
+ return transformedValue;
+}
+
+@end
+
+@implementation MTLReversibleValueTransformer
+
+#pragma mark Lifecycle
+
+- (id)initWithForwardBlock:(MTLValueTransformerBlock)forwardBlock reverseBlock:(MTLValueTransformerBlock)reverseBlock {
+ NSParameterAssert(reverseBlock != nil);
+ return [super initWithForwardBlock:forwardBlock reverseBlock:reverseBlock];
+}
+
+#pragma mark NSValueTransformer
+
++ (BOOL)allowsReverseTransformation {
+ return YES;
+}
+
+- (id)reverseTransformedValue:(id)value {
+ NSError *error = nil;
+ BOOL success = YES;
+
+ return self.reverseBlock(value, &success, &error);
+}
+
+- (id)reverseTransformedValue:(id)value success:(BOOL *)outerSuccess error:(NSError **)outerError {
+ NSError *error = nil;
+ BOOL success = YES;
+
+ id transformedValue = self.reverseBlock(value, &success, &error);
+
+ if (outerSuccess != NULL) *outerSuccess = success;
+ if (outerError != NULL) *outerError = error;
+
+ return transformedValue;
+}
+
+@end
+
+
+@implementation MTLValueTransformer (Deprecated)
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-implementations"
+
++ (instancetype)transformerWithBlock:(id (^)(id))transformationBlock {
+ return [self transformerUsingForwardBlock:^(id value, BOOL *success, NSError **error) {
+ return transformationBlock(value);
+ }];
+}
+
++ (instancetype)reversibleTransformerWithBlock:(id (^)(id))transformationBlock {
+ return [self transformerUsingReversibleBlock:^(id value, BOOL *success, NSError **error) {
+ return transformationBlock(value);
+ }];
+}
+
++ (instancetype)reversibleTransformerWithForwardBlock:(id (^)(id))forwardBlock reverseBlock:(id (^)(id))reverseBlock {
+ return [self
+ transformerUsingForwardBlock:^(id value, BOOL *success, NSError **error) {
+ return forwardBlock(value);
+ }
+ reverseBlock:^(id value, BOOL *success, NSError **error) {
+ return reverseBlock(value);
+ }];
+}
+
+#pragma clang diagnostic pop
+
+@end
diff --git a/Apps/Wikipedia/WMF Framework/Third Party/Mantle/NSArray+MTLManipulationAdditions.m b/Apps/Wikipedia/WMF Framework/Third Party/Mantle/NSArray+MTLManipulationAdditions.m
new file mode 100644
index 0000000..e6932c9
--- /dev/null
+++ b/Apps/Wikipedia/WMF Framework/Third Party/Mantle/NSArray+MTLManipulationAdditions.m
@@ -0,0 +1,42 @@
+//
+// NSArray+MTLManipulationAdditions.m
+// Mantle
+//
+// Created by Josh Abernathy on 9/19/12.
+// Copyright (c) 2012 GitHub. All rights reserved.
+//
+
+#import "NSArray+MTLManipulationAdditions.h"
+
+@interface NSArray (MTLDeclarations)
+
+// This declaration is needed so Mantle can be compiled with SDK 6 / 10.8.
+- (id)firstObject;
+
+@end
+
+@implementation NSArray (MTLManipulationAdditions)
+
+- (id)mtl_firstObject {
+ return self.firstObject;
+}
+
+- (instancetype)mtl_arrayByRemovingObject:(id)object {
+ NSMutableArray *result = [self mutableCopy];
+ [result removeObject:object];
+ return result;
+}
+
+- (instancetype)mtl_arrayByRemovingFirstObject {
+ if (self.count == 0) return self;
+
+ return [self subarrayWithRange:NSMakeRange(1, self.count - 1)];
+}
+
+- (instancetype)mtl_arrayByRemovingLastObject {
+ if (self.count == 0) return self;
+
+ return [self subarrayWithRange:NSMakeRange(0, self.count - 1)];
+}
+
+@end
diff --git a/Apps/Wikipedia/WMF Framework/Third Party/Mantle/NSDictionary+MTLJSONKeyPath.h b/Apps/Wikipedia/WMF Framework/Third Party/Mantle/NSDictionary+MTLJSONKeyPath.h
new file mode 100644
index 0000000..eb70151
--- /dev/null
+++ b/Apps/Wikipedia/WMF Framework/Third Party/Mantle/NSDictionary+MTLJSONKeyPath.h
@@ -0,0 +1,27 @@
+//
+// NSDictionary+MTLJSONKeyPath.h
+// Mantle
+//
+// Created by Robert Böhnke on 19/03/14.
+// Copyright (c) 2014 GitHub. All rights reserved.
+//
+
+#import
+
+@interface NSDictionary (MTLJSONKeyPath)
+
+/// Looks up the value of a key path in the receiver.
+///
+/// JSONKeyPath - The key path that should be resolved. Every element along this
+/// key path needs to be an instance of NSDictionary for the
+/// resolving to be successful.
+/// success - If not NULL, this will be set to a boolean indicating whether
+/// the key path was resolved successfully.
+/// error - If not NULL, this may be set to an error that occurs during
+/// resolving the value.
+///
+/// Returns the value for the key path which may be nil. Clients should inspect
+/// the success parameter to decide how to proceed with the result.
+- (id)mtl_valueForJSONKeyPath:(NSString *)JSONKeyPath success:(BOOL *)success error:(NSError **)error;
+
+@end
diff --git a/Apps/Wikipedia/WMF Framework/Third Party/Mantle/NSDictionary+MTLJSONKeyPath.m b/Apps/Wikipedia/WMF Framework/Third Party/Mantle/NSDictionary+MTLJSONKeyPath.m
new file mode 100644
index 0000000..03ee2ad
--- /dev/null
+++ b/Apps/Wikipedia/WMF Framework/Third Party/Mantle/NSDictionary+MTLJSONKeyPath.m
@@ -0,0 +1,47 @@
+//
+// NSDictionary+MTLJSONKeyPath.m
+// Mantle
+//
+// Created by Robert Böhnke on 19/03/14.
+// Copyright (c) 2014 GitHub. All rights reserved.
+//
+
+#import "NSDictionary+MTLJSONKeyPath.h"
+
+#import "MTLJSONAdapter.h"
+
+@implementation NSDictionary (MTLJSONKeyPath)
+
+- (id)mtl_valueForJSONKeyPath:(NSString *)JSONKeyPath success:(BOOL *)success error:(NSError **)error {
+ NSArray *components = [JSONKeyPath componentsSeparatedByString:@"."];
+
+ id result = self;
+ for (NSString *component in components) {
+ // Check the result before resolving the key path component to not
+ // affect the last value of the path.
+ if (result == nil || result == NSNull.null) break;
+
+ if (![result isKindOfClass:NSDictionary.class]) {
+ if (error != NULL) {
+ NSDictionary *userInfo = @{
+ NSLocalizedDescriptionKey: NSLocalizedString(@"Invalid JSON dictionary", @""),
+ NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"JSON key path %1$@ could not resolved because an incompatible JSON dictionary was supplied: \"%2$@\"", @""), JSONKeyPath, self]
+ };
+
+ *error = [NSError errorWithDomain:MTLJSONAdapterErrorDomain code:MTLJSONAdapterErrorInvalidJSONDictionary userInfo:userInfo];
+ }
+
+ if (success != NULL) *success = NO;
+
+ return nil;
+ }
+
+ result = result[component];
+ }
+
+ if (success != NULL) *success = YES;
+
+ return result;
+}
+
+@end
diff --git a/Apps/Wikipedia/WMF Framework/Third Party/Mantle/NSDictionary+MTLManipulationAdditions.m b/Apps/Wikipedia/WMF Framework/Third Party/Mantle/NSDictionary+MTLManipulationAdditions.m
new file mode 100644
index 0000000..a7a1d7a
--- /dev/null
+++ b/Apps/Wikipedia/WMF Framework/Third Party/Mantle/NSDictionary+MTLManipulationAdditions.m
@@ -0,0 +1,38 @@
+//
+// NSDictionary+MTLManipulationAdditions.m
+// Mantle
+//
+// Created by Justin Spahr-Summers on 2012-09-24.
+// Copyright (c) 2012 GitHub. All rights reserved.
+//
+
+#import "NSDictionary+MTLManipulationAdditions.h"
+
+@implementation NSDictionary (MTLManipulationAdditions)
+
+- (NSDictionary *)mtl_dictionaryByAddingEntriesFromDictionary:(NSDictionary *)dictionary {
+ NSMutableDictionary *result = [self mutableCopy];
+ [result addEntriesFromDictionary:dictionary];
+ return result;
+}
+
+- (NSDictionary *)mtl_dictionaryByRemovingValuesForKeys:(NSArray *)keys {
+ NSMutableDictionary *result = [self mutableCopy];
+ [result removeObjectsForKeys:keys];
+ return result;
+}
+
+@end
+
+@implementation NSDictionary (MTLManipulationAdditions_Deprecated)
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated"
+
+- (NSDictionary *)mtl_dictionaryByRemovingEntriesWithKeys:(NSSet *)keys {
+ return [self mtl_dictionaryByRemovingValuesForKeys:keys.allObjects];
+}
+
+#pragma clang diagnostic pop
+
+@end
diff --git a/Apps/Wikipedia/WMF Framework/Third Party/Mantle/NSDictionary+MTLMappingAdditions.m b/Apps/Wikipedia/WMF Framework/Third Party/Mantle/NSDictionary+MTLMappingAdditions.m
new file mode 100644
index 0000000..6071906
--- /dev/null
+++ b/Apps/Wikipedia/WMF Framework/Third Party/Mantle/NSDictionary+MTLMappingAdditions.m
@@ -0,0 +1,23 @@
+//
+// NSDictionary+MTLMappingAdditions.m
+// Mantle
+//
+// Created by Robert Böhnke on 10/31/13.
+// Copyright (c) 2013 GitHub. All rights reserved.
+//
+
+#import "MTLModel.h"
+
+#import "NSDictionary+MTLMappingAdditions.h"
+
+@implementation NSDictionary (MTLMappingAdditions)
+
++ (NSDictionary *)mtl_identityPropertyMapWithModel:(Class)modelClass {
+ NSCParameterAssert([modelClass conformsToProtocol:@protocol(MTLModel)]);
+
+ NSArray *propertyKeys = [modelClass propertyKeys].allObjects;
+
+ return [NSDictionary dictionaryWithObjects:propertyKeys forKeys:propertyKeys];
+}
+
+@end
diff --git a/Apps/Wikipedia/WMF Framework/Third Party/Mantle/NSError+MTLModelException.h b/Apps/Wikipedia/WMF Framework/Third Party/Mantle/NSError+MTLModelException.h
new file mode 100644
index 0000000..852fbdb
--- /dev/null
+++ b/Apps/Wikipedia/WMF Framework/Third Party/Mantle/NSError+MTLModelException.h
@@ -0,0 +1,23 @@
+//
+// NSError+MTLModelException.h
+// Mantle
+//
+// Created by Robert Böhnke on 7/6/13.
+// Copyright (c) 2013 GitHub. All rights reserved.
+//
+
+#import
+
+@interface NSError (MTLModelException)
+
+/// Creates a new error for an exception that occurred during updating an
+/// MTLModel.
+///
+/// exception - The exception that was thrown while updating the model.
+/// This argument must not be nil.
+///
+/// Returns an error that takes its localized description and failure reason
+/// from the exception.
++ (instancetype)mtl_modelErrorWithException:(NSException *)exception;
+
+@end
diff --git a/Apps/Wikipedia/WMF Framework/Third Party/Mantle/NSError+MTLModelException.m b/Apps/Wikipedia/WMF Framework/Third Party/Mantle/NSError+MTLModelException.m
new file mode 100644
index 0000000..8b71e06
--- /dev/null
+++ b/Apps/Wikipedia/WMF Framework/Third Party/Mantle/NSError+MTLModelException.m
@@ -0,0 +1,36 @@
+//
+// NSError+MTLModelException.m
+// Mantle
+//
+// Created by Robert Böhnke on 7/6/13.
+// Copyright (c) 2013 GitHub. All rights reserved.
+//
+
+#import "MTLModel.h"
+
+#import "NSError+MTLModelException.h"
+
+// The domain for errors originating from MTLModel.
+static NSString * const MTLModelErrorDomain = @"MTLModelErrorDomain";
+
+// An exception was thrown and caught.
+static const NSInteger MTLModelErrorExceptionThrown = 1;
+
+// Associated with the NSException that was caught.
+static NSString * const MTLModelThrownExceptionErrorKey = @"MTLModelThrownException";
+
+@implementation NSError (MTLModelException)
+
++ (instancetype)mtl_modelErrorWithException:(NSException *)exception {
+ NSParameterAssert(exception != nil);
+
+ NSDictionary *userInfo = @{
+ NSLocalizedDescriptionKey: exception.description,
+ NSLocalizedFailureReasonErrorKey: exception.reason,
+ MTLModelThrownExceptionErrorKey: exception
+ };
+
+ return [NSError errorWithDomain:MTLModelErrorDomain code:MTLModelErrorExceptionThrown userInfo:userInfo];
+}
+
+@end
diff --git a/Apps/Wikipedia/WMF Framework/Third Party/Mantle/NSObject+MTLComparisonAdditions.m b/Apps/Wikipedia/WMF Framework/Third Party/Mantle/NSObject+MTLComparisonAdditions.m
new file mode 100644
index 0000000..3b77b35
--- /dev/null
+++ b/Apps/Wikipedia/WMF Framework/Third Party/Mantle/NSObject+MTLComparisonAdditions.m
@@ -0,0 +1,16 @@
+//
+// NSObject+MTLComparisonAdditions.m
+// Mantle
+//
+// Created by Josh Vera on 10/26/12.
+// Copyright (c) 2012 GitHub. All rights reserved.
+//
+// Portions copyright (c) 2011 Bitswift. All rights reserved.
+// See the LICENSE file for more information.
+//
+
+#import "NSObject+MTLComparisonAdditions.h"
+
+BOOL MTLEqualObjects(id obj1, id obj2) {
+ return (obj1 == obj2 || [obj1 isEqual:obj2]);
+}
diff --git a/Apps/Wikipedia/WMF Framework/Third Party/Mantle/NSValueTransformer+MTLInversionAdditions.m b/Apps/Wikipedia/WMF Framework/Third Party/Mantle/NSValueTransformer+MTLInversionAdditions.m
new file mode 100644
index 0000000..2e536d6
--- /dev/null
+++ b/Apps/Wikipedia/WMF Framework/Third Party/Mantle/NSValueTransformer+MTLInversionAdditions.m
@@ -0,0 +1,37 @@
+//
+// NSValueTransformer+MTLInversionAdditions.m
+// Mantle
+//
+// Created by Justin Spahr-Summers on 2013-05-18.
+// Copyright (c) 2013 GitHub. All rights reserved.
+//
+
+#import "NSValueTransformer+MTLInversionAdditions.h"
+#import "MTLTransformerErrorHandling.h"
+#import "MTLValueTransformer.h"
+
+@implementation NSValueTransformer (MTLInversionAdditions)
+
+- (NSValueTransformer *)mtl_invertedTransformer {
+ NSParameterAssert(self.class.allowsReverseTransformation);
+
+ if ([self conformsToProtocol:@protocol(MTLTransformerErrorHandling)]) {
+ NSParameterAssert([self respondsToSelector:@selector(reverseTransformedValue:success:error:)]);
+
+ id errorHandlingSelf = (id)self;
+
+ return [MTLValueTransformer transformerUsingForwardBlock:^(id value, BOOL *success, NSError **error) {
+ return [errorHandlingSelf reverseTransformedValue:value success:success error:error];
+ } reverseBlock:^(id value, BOOL *success, NSError **error) {
+ return [errorHandlingSelf transformedValue:value success:success error:error];
+ }];
+ } else {
+ return [MTLValueTransformer transformerUsingForwardBlock:^(id value, BOOL *success, NSError **error) {
+ return [self reverseTransformedValue:value];
+ } reverseBlock:^(id value, BOOL *success, NSError **error) {
+ return [self transformedValue:value];
+ }];
+ }
+}
+
+@end
diff --git a/Apps/Wikipedia/WMF Framework/Third Party/Mantle/NSValueTransformer+MTLPredefinedTransformerAdditions.m b/Apps/Wikipedia/WMF Framework/Third Party/Mantle/NSValueTransformer+MTLPredefinedTransformerAdditions.m
new file mode 100644
index 0000000..2ef182b
--- /dev/null
+++ b/Apps/Wikipedia/WMF Framework/Third Party/Mantle/NSValueTransformer+MTLPredefinedTransformerAdditions.m
@@ -0,0 +1,447 @@
+//
+// NSValueTransformer+MTLPredefinedTransformerAdditions.m
+// Mantle
+//
+// Created by Justin Spahr-Summers on 2012-09-27.
+// Copyright (c) 2012 GitHub. All rights reserved.
+//
+
+#import "NSValueTransformer+MTLPredefinedTransformerAdditions.h"
+#import "MTLJSONAdapter.h"
+#import "MTLModel.h"
+#import "MTLValueTransformer.h"
+
+NSString * const MTLURLValueTransformerName = @"MTLURLValueTransformerName";
+NSString * const MTLUUIDValueTransformerName = @"MTLUUIDValueTransformerName";
+NSString * const MTLBooleanValueTransformerName = @"MTLBooleanValueTransformerName";
+
+@implementation NSValueTransformer (MTLPredefinedTransformerAdditions)
+
+#pragma mark Category Loading
+
++ (void)load {
+ @autoreleasepool {
+ MTLValueTransformer *URLValueTransformer = [MTLValueTransformer
+ transformerUsingForwardBlock:^ id (NSString *str, BOOL *success, NSError **error) {
+ if (str == nil) return nil;
+
+ if (![str isKindOfClass:NSString.class]) {
+ if (error != NULL) {
+ NSDictionary *userInfo = @{
+ NSLocalizedDescriptionKey: NSLocalizedString(@"Could not convert string to URL", @""),
+ NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"Expected an NSString, got: %@.", @""), str],
+ MTLTransformerErrorHandlingInputValueErrorKey : str
+ };
+
+ *error = [NSError errorWithDomain:MTLTransformerErrorHandlingErrorDomain code:MTLTransformerErrorHandlingErrorInvalidInput userInfo:userInfo];
+ }
+ *success = NO;
+ return nil;
+ }
+
+ NSURL *result = [NSURL URLWithString:str];
+
+ if (result == nil) {
+ if (error != NULL) {
+ NSDictionary *userInfo = @{
+ NSLocalizedDescriptionKey: NSLocalizedString(@"Could not convert string to URL", @""),
+ NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"Input URL string %@ was malformed", @""), str],
+ MTLTransformerErrorHandlingInputValueErrorKey : str
+ };
+
+ *error = [NSError errorWithDomain:MTLTransformerErrorHandlingErrorDomain code:MTLTransformerErrorHandlingErrorInvalidInput userInfo:userInfo];
+ }
+ *success = NO;
+ return nil;
+ }
+
+ return result;
+ }
+ reverseBlock:^ id (NSURL *URL, BOOL *success, NSError **error) {
+ if (URL == nil) return nil;
+
+ if (![URL isKindOfClass:NSURL.class]) {
+ if (error != NULL) {
+ NSDictionary *userInfo = @{
+ NSLocalizedDescriptionKey: NSLocalizedString(@"Could not convert URL to string", @""),
+ NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"Expected an NSURL, got: %@.", @""), URL],
+ MTLTransformerErrorHandlingInputValueErrorKey : URL
+ };
+
+ *error = [NSError errorWithDomain:MTLTransformerErrorHandlingErrorDomain code:MTLTransformerErrorHandlingErrorInvalidInput userInfo:userInfo];
+ }
+ *success = NO;
+ return nil;
+ }
+ return URL.absoluteString;
+ }];
+
+ [NSValueTransformer setValueTransformer:URLValueTransformer forName:MTLURLValueTransformerName];
+
+ MTLValueTransformer *UUIDValueTransformer = [MTLValueTransformer
+ transformerUsingForwardBlock:^id(NSString *string, BOOL *success, NSError **error) {
+ if (string == nil) return nil;
+
+ if (![string isKindOfClass:NSString.class]) {
+ if (error) {
+ NSDictionary *userInfo = @{
+ NSLocalizedDescriptionKey: NSLocalizedString(@"Could not convert string to UUID", @""),
+ NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"Expected an NSString, got: %@.", @""), string],
+ MTLTransformerErrorHandlingInputValueErrorKey : string
+ };
+ *error = [NSError errorWithDomain:MTLTransformerErrorHandlingErrorDomain code:MTLTransformerErrorHandlingErrorInvalidInput userInfo:userInfo];
+ }
+ *success = NO;
+ return nil;
+ }
+
+ NSUUID *result = [[NSUUID alloc] initWithUUIDString:string];
+
+ if (result == nil) {
+ if (error) {
+ NSDictionary *userInfo = @{
+ NSLocalizedDescriptionKey: NSLocalizedString(@"Could not convert string to UUID", @""),
+ NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"Input UUID string %@ was malformed", @""), string],
+ MTLTransformerErrorHandlingInputValueErrorKey : string
+ };
+ *error = [NSError errorWithDomain:MTLTransformerErrorHandlingErrorDomain code:MTLTransformerErrorHandlingErrorInvalidInput userInfo:userInfo];
+ }
+ *success = NO;
+ return nil;
+ }
+
+ return result;
+ }
+ reverseBlock:^id(NSUUID *uuid, BOOL *success, NSError **error) {
+ if (uuid == nil) return nil;
+
+ if (![uuid isKindOfClass:NSUUID.class]) {
+ if (error != NULL) {
+ NSDictionary *userInfo = @{NSLocalizedDescriptionKey: NSLocalizedString(@"Could not convert UUID to string", @""),
+ NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"Expected an NSUUID, got: %@.", @""), uuid],
+ MTLTransformerErrorHandlingInputValueErrorKey : uuid};
+ *error = [NSError errorWithDomain:MTLTransformerErrorHandlingErrorDomain code:MTLTransformerErrorHandlingErrorInvalidInput userInfo:userInfo];
+ }
+ *success = NO;
+ return nil;
+ }
+
+ return uuid.UUIDString;
+ }];
+
+ [NSValueTransformer setValueTransformer:UUIDValueTransformer forName:MTLUUIDValueTransformerName];
+
+ MTLValueTransformer *booleanValueTransformer = [MTLValueTransformer
+ transformerUsingReversibleBlock:^ id (NSNumber *boolean, BOOL *success, NSError **error) {
+ if (boolean == nil) return nil;
+
+ if (![boolean isKindOfClass:NSNumber.class]) {
+ if (error != NULL) {
+ NSDictionary *userInfo = @{
+ NSLocalizedDescriptionKey: NSLocalizedString(@"Could not convert number to boolean-backed number or vice-versa", @""),
+ NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"Expected an NSNumber, got: %@.", @""), boolean],
+ MTLTransformerErrorHandlingInputValueErrorKey : boolean
+ };
+
+ *error = [NSError errorWithDomain:MTLTransformerErrorHandlingErrorDomain code:MTLTransformerErrorHandlingErrorInvalidInput userInfo:userInfo];
+ }
+ *success = NO;
+ return nil;
+ }
+ return (NSNumber *)(boolean.boolValue ? kCFBooleanTrue : kCFBooleanFalse);
+ }];
+
+ [NSValueTransformer setValueTransformer:booleanValueTransformer forName:MTLBooleanValueTransformerName];
+ }
+}
+
+#pragma mark Customizable Transformers
+
++ (NSValueTransformer *)mtl_arrayMappingTransformerWithTransformer:(NSValueTransformer *)transformer {
+ NSParameterAssert(transformer != nil);
+
+ id (^forwardBlock)(NSArray *values, BOOL *success, NSError **error) = ^ id (NSArray *values, BOOL *success, NSError **error) {
+ if (values == nil) return nil;
+
+ if (![values isKindOfClass:NSArray.class]) {
+ if (error != NULL) {
+ NSDictionary *userInfo = @{
+ NSLocalizedDescriptionKey: NSLocalizedString(@"Could not transform non-array type", @""),
+ NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"Expected an NSArray, got: %@.", @""), values],
+ MTLTransformerErrorHandlingInputValueErrorKey: values
+ };
+
+ *error = [NSError errorWithDomain:MTLTransformerErrorHandlingErrorDomain code:MTLTransformerErrorHandlingErrorInvalidInput userInfo:userInfo];
+ }
+ *success = NO;
+ return nil;
+ }
+
+ NSMutableArray *transformedValues = [NSMutableArray arrayWithCapacity:values.count];
+ NSInteger index = -1;
+ for (id value in values) {
+ index++;
+ if (value == NSNull.null) {
+ [transformedValues addObject:NSNull.null];
+ continue;
+ }
+
+ id transformedValue = nil;
+ if ([transformer conformsToProtocol:@protocol(MTLTransformerErrorHandling)]) {
+ NSError *underlyingError = nil;
+ transformedValue = [(id)transformer transformedValue:value success:success error:&underlyingError];
+
+ if (*success == NO) {
+ if (error != NULL) {
+ NSDictionary *userInfo = @{
+ NSLocalizedDescriptionKey: NSLocalizedString(@"Could not transform array", @""),
+ NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"Could not transform value at index %ld", @""), (long)index],
+ NSUnderlyingErrorKey: underlyingError,
+ MTLTransformerErrorHandlingInputValueErrorKey: values
+ };
+
+ *error = [NSError errorWithDomain:MTLTransformerErrorHandlingErrorDomain code:MTLTransformerErrorHandlingErrorInvalidInput userInfo:userInfo];
+ }
+ return nil;
+ }
+ } else {
+ transformedValue = [transformer transformedValue:value];
+ }
+
+ if (transformedValue == nil) continue;
+
+ [transformedValues addObject:transformedValue];
+ }
+
+ return transformedValues;
+ };
+
+ id (^reverseBlock)(NSArray *values, BOOL *success, NSError **error) = nil;
+ if (transformer.class.allowsReverseTransformation) {
+ reverseBlock = ^ id (NSArray *values, BOOL *success, NSError **error) {
+ if (values == nil) return nil;
+
+ if (![values isKindOfClass:NSArray.class]) {
+ if (error != NULL) {
+ NSDictionary *userInfo = @{
+ NSLocalizedDescriptionKey: NSLocalizedString(@"Could not transform non-array type", @""),
+ NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"Expected an NSArray, got: %@.", @""), values],
+ MTLTransformerErrorHandlingInputValueErrorKey: values
+ };
+
+ *error = [NSError errorWithDomain:MTLTransformerErrorHandlingErrorDomain code:MTLTransformerErrorHandlingErrorInvalidInput userInfo:userInfo];
+ }
+ *success = NO;
+ return nil;
+ }
+
+ NSMutableArray *transformedValues = [NSMutableArray arrayWithCapacity:values.count];
+ NSInteger index = -1;
+ for (id value in values) {
+ index++;
+ if (value == NSNull.null) {
+ [transformedValues addObject:NSNull.null];
+
+ continue;
+ }
+
+ id transformedValue = nil;
+ if ([transformer respondsToSelector:@selector(reverseTransformedValue:success:error:)]) {
+ NSError *underlyingError = nil;
+ transformedValue = [(id)transformer reverseTransformedValue:value success:success error:&underlyingError];
+
+ if (*success == NO) {
+ if (error != NULL) {
+ NSDictionary *userInfo = @{
+ NSLocalizedDescriptionKey: NSLocalizedString(@"Could not transform array", @""),
+ NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"Could not transform value at index %ld", @""), (long)index],
+ NSUnderlyingErrorKey: underlyingError,
+ MTLTransformerErrorHandlingInputValueErrorKey: values
+ };
+
+ *error = [NSError errorWithDomain:MTLTransformerErrorHandlingErrorDomain code:MTLTransformerErrorHandlingErrorInvalidInput userInfo:userInfo];
+ }
+ return nil;
+ }
+ } else {
+ transformedValue = [transformer reverseTransformedValue:value];
+ }
+
+ if (transformedValue == nil) continue;
+
+ [transformedValues addObject:transformedValue];
+ }
+
+ return transformedValues;
+ };
+ }
+ if (reverseBlock != nil) {
+ return [MTLValueTransformer transformerUsingForwardBlock:forwardBlock reverseBlock:reverseBlock];
+ } else {
+ return [MTLValueTransformer transformerUsingForwardBlock:forwardBlock];
+ }
+}
+
++ (NSValueTransformer *)mtl_validatingTransformerForClass:(Class)modelClass {
+ NSParameterAssert(modelClass != nil);
+
+ return [MTLValueTransformer transformerUsingForwardBlock:^ id (id value, BOOL *success, NSError **error) {
+ if (value != nil && ![value isKindOfClass:modelClass]) {
+ if (error != NULL) {
+ NSDictionary *userInfo = @{
+ NSLocalizedDescriptionKey: NSLocalizedString(@"Value did not match expected type", @""),
+ NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"Expected %1$@ to be of class %2$@ but got %3$@", @""), value, modelClass, [value class]],
+ MTLTransformerErrorHandlingInputValueErrorKey : value
+ };
+
+ *error = [NSError errorWithDomain:MTLTransformerErrorHandlingErrorDomain code:MTLTransformerErrorHandlingErrorInvalidInput userInfo:userInfo];
+ }
+ *success = NO;
+ return nil;
+ }
+
+ return value;
+ }];
+}
+
++ (NSValueTransformer *)mtl_valueMappingTransformerWithDictionary:(NSDictionary *)dictionary defaultValue:(id)defaultValue reverseDefaultValue:(id)reverseDefaultValue {
+ NSParameterAssert(dictionary != nil);
+ NSParameterAssert(dictionary.count == [[NSSet setWithArray:dictionary.allValues] count]);
+
+ return [MTLValueTransformer
+ transformerUsingForwardBlock:^ id (id key, BOOL *success, NSError **error) {
+ return dictionary[key ?: NSNull.null] ?: defaultValue;
+ }
+ reverseBlock:^ id (id value, BOOL *success, NSError **error) {
+ __block id result = nil;
+ [dictionary enumerateKeysAndObjectsUsingBlock:^(id key, id anObject, BOOL *stop) {
+ if ([value isEqual:anObject]) {
+ result = key;
+ *stop = YES;
+ }
+ }];
+
+ return result ?: reverseDefaultValue;
+ }];
+}
+
++ (NSValueTransformer *)mtl_valueMappingTransformerWithDictionary:(NSDictionary *)dictionary {
+ return [self mtl_valueMappingTransformerWithDictionary:dictionary defaultValue:nil reverseDefaultValue:nil];
+}
+
++ (NSValueTransformer *)mtl_dateTransformerWithDateFormat:(NSString *)dateFormat calendar:(NSCalendar *)calendar locale:(NSLocale *)locale timeZone:(NSTimeZone *)timeZone defaultDate:(NSDate *)defaultDate {
+ NSParameterAssert(dateFormat.length);
+
+ NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
+ dateFormatter.dateFormat = dateFormat;
+ dateFormatter.calendar = calendar;
+ dateFormatter.locale = locale;
+ dateFormatter.timeZone = timeZone;
+ dateFormatter.defaultDate = defaultDate;
+
+ return [NSValueTransformer mtl_transformerWithFormatter:dateFormatter forObjectClass:NSDate.class];
+}
+
+
++ (NSValueTransformer *)mtl_dateTransformerWithDateFormat:(NSString *)dateFormat locale:(NSLocale *)locale {
+ return [self mtl_dateTransformerWithDateFormat:dateFormat calendar:nil locale:locale timeZone:nil defaultDate:nil];
+}
+
++ (NSValueTransformer *)mtl_numberTransformerWithNumberStyle:(NSNumberFormatterStyle)numberStyle locale:(NSLocale *)locale {
+ NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
+ numberFormatter.numberStyle = numberStyle;
+ numberFormatter.locale = locale;
+
+ return [self mtl_transformerWithFormatter:numberFormatter forObjectClass:NSNumber.class];
+}
+
++ (NSValueTransformer *)mtl_transformerWithFormatter:(NSFormatter *)formatter forObjectClass:(Class)objectClass {
+ NSParameterAssert(formatter != nil);
+ NSParameterAssert(objectClass != nil);
+ return [MTLValueTransformer
+ transformerUsingForwardBlock:^ id (NSString *str, BOOL *success, NSError *__autoreleasing *error) {
+ if (str == nil) return nil;
+
+ if (![str isKindOfClass:NSString.class]) {
+ if (error != NULL) {
+ NSDictionary *userInfo = @{
+ NSLocalizedDescriptionKey: [NSString stringWithFormat:NSLocalizedString(@"Could not convert string to %@", @""), objectClass],
+ NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"Expected an NSString as input, got: %@.", @""), str],
+ MTLTransformerErrorHandlingInputValueErrorKey : str
+ };
+
+ *error = [NSError errorWithDomain:MTLTransformerErrorHandlingErrorDomain code:MTLTransformerErrorHandlingErrorInvalidInput userInfo:userInfo];
+ }
+ *success = NO;
+ return nil;
+ }
+
+ id object = nil;
+ NSString *errorDescription = nil;
+ *success = [formatter getObjectValue:&object forString:str errorDescription:&errorDescription];
+
+ if (errorDescription != nil) {
+ if (error != NULL) {
+ NSDictionary *userInfo = @{
+ NSLocalizedDescriptionKey: [NSString stringWithFormat:NSLocalizedString(@"Could not convert string to %@", @""), objectClass],
+ NSLocalizedFailureReasonErrorKey: errorDescription,
+ MTLTransformerErrorHandlingInputValueErrorKey : str
+ };
+
+ *error = [NSError errorWithDomain:MTLTransformerErrorHandlingErrorDomain code:MTLTransformerErrorHandlingErrorInvalidInput userInfo:userInfo];
+ }
+ *success = NO;
+ return nil;
+ }
+
+ if (![object isKindOfClass:objectClass]) {
+ if (error != NULL) {
+ NSDictionary *userInfo = @{
+ NSLocalizedDescriptionKey: [NSString stringWithFormat:NSLocalizedString(@"Could not convert string to %@", @""), objectClass],
+ NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"Expected an %@ as output from the formatter, got: %@.", @""), objectClass, object],
+ };
+
+ *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFormattingError userInfo:userInfo];
+ }
+ *success = NO;
+ return nil;
+ }
+
+ return object;
+ } reverseBlock:^id(id object, BOOL *success, NSError *__autoreleasing *error) {
+ if (object == nil) return nil;
+
+ if (![object isKindOfClass:objectClass]) {
+ if (error != NULL) {
+ NSDictionary *userInfo = @{
+ NSLocalizedDescriptionKey: [NSString stringWithFormat:NSLocalizedString(@"Could not convert %@ to string", @""), objectClass],
+ NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"Expected an %@ as input, got: %@.", @""), objectClass, object],
+ MTLTransformerErrorHandlingInputValueErrorKey : object
+ };
+
+ *error = [NSError errorWithDomain:MTLTransformerErrorHandlingErrorDomain code:MTLTransformerErrorHandlingErrorInvalidInput userInfo:userInfo];
+ }
+ *success = NO;
+ return nil;
+ }
+
+ NSString *string = [formatter stringForObjectValue:object];
+ *success = (string != nil);
+ return string;
+ }];
+}
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-implementations"
+
++ (NSValueTransformer *)mtl_JSONDictionaryTransformerWithModelClass:(Class)modelClass {
+ return [MTLJSONAdapter dictionaryTransformerWithModelClass:modelClass];
+}
+
++ (NSValueTransformer *)mtl_JSONArrayTransformerWithModelClass:(Class)modelClass {
+ return [MTLJSONAdapter arrayTransformerWithModelClass:modelClass];
+}
+
+#pragma clang diagnostic pop
+
+@end
diff --git a/Apps/Wikipedia/WMF Framework/Third Party/Mantle/extobjc/MTLEXTRuntimeExtensions.m b/Apps/Wikipedia/WMF Framework/Third Party/Mantle/extobjc/MTLEXTRuntimeExtensions.m
new file mode 100644
index 0000000..4a4f397
--- /dev/null
+++ b/Apps/Wikipedia/WMF Framework/Third Party/Mantle/extobjc/MTLEXTRuntimeExtensions.m
@@ -0,0 +1,944 @@
+//
+// MTLEXTRuntimeExtensions.m
+// extobjc
+//
+// Created by Justin Spahr-Summers on 2011-03-05.
+// Copyright (C) 2012 Justin Spahr-Summers.
+// Released under the MIT license.
+//
+
+#import "MTLEXTRuntimeExtensions.h"
+#import
+#import
+#import
+#import
+#import
+#import
+#import
+#import
+
+typedef NSMethodSignature *(*methodSignatureForSelectorIMP)(id, SEL, SEL);
+typedef void (^mtl_specialProtocolInjectionBlock)(Class);
+
+// a `const char *` equivalent to system struct objc_method_description
+typedef struct {
+ SEL name;
+ const char *types;
+} mtl_methodDescription;
+
+// contains the information needed to reference a full special protocol
+typedef struct {
+ // the actual protocol declaration (@protocol block)
+ __unsafe_unretained Protocol *protocol;
+
+ // the injection block associated with this protocol
+ //
+ // this block is RETAINED and must eventually be released by transferring it
+ // back to ARC
+ void *injectionBlock;
+
+ // whether this protocol is ready to be injected to its conforming classes
+ //
+ // this does NOT refer to a special protocol having been injected already
+ BOOL ready;
+} MTLSpecialProtocol;
+
+// the full list of special protocols (an array of MTLSpecialProtocol structs)
+static MTLSpecialProtocol * restrict specialProtocols = NULL;
+
+// the number of special protocols stored in the array
+static size_t specialProtocolCount = 0;
+
+// the total capacity of the array
+// we use a doubling algorithm to amortize the cost of insertion, so this is
+// generally going to be a power-of-two
+static size_t specialProtocolCapacity = 0;
+
+// the number of MTLSpecialProtocols which have been marked as ready for
+// injection (though not necessary injected)
+//
+// in other words, the total count which have 'ready' set to YES
+static size_t specialProtocolsReady = 0;
+
+// a mutex is used to guard against multiple threads changing the above static
+// variables
+static pthread_mutex_t specialProtocolsLock = PTHREAD_MUTEX_INITIALIZER;
+
+/**
+ * This function actually performs the hard work of special protocol injection.
+ * It obtains a full list of all classes registered with the Objective-C
+ * runtime, finds those conforming to special protocols, and then runs the
+ * injection blocks as appropriate.
+ */
+static void mtl_injectSpecialProtocols (void) {
+ /*
+ * don't lock specialProtocolsLock in this function, as it is called only
+ * from public functions which already perform the synchronization
+ */
+
+ /*
+ * This will sort special protocols in the order they should be loaded. If
+ * a special protocol conforms to another special protocol, the former
+ * will be prioritized above the latter.
+ */
+ qsort_b(specialProtocols, specialProtocolCount, sizeof(MTLSpecialProtocol), ^(const void *a, const void *b){
+ // if the pointers are equal, it must be the same protocol
+ if (a == b)
+ return 0;
+
+ const MTLSpecialProtocol *protoA = a;
+ const MTLSpecialProtocol *protoB = b;
+
+ // A higher return value here means a higher priority
+ int (^protocolInjectionPriority)(const MTLSpecialProtocol *) = ^(const MTLSpecialProtocol *specialProtocol){
+ int runningTotal = 0;
+
+ for (size_t i = 0;i < specialProtocolCount;++i) {
+ // the pointer passed into this block is guaranteed to point
+ // into the 'specialProtocols' array, so we can compare the
+ // pointers directly for identity
+ if (specialProtocol == specialProtocols + i)
+ continue;
+
+ if (protocol_conformsToProtocol(specialProtocol->protocol, specialProtocols[i].protocol))
+ runningTotal++;
+ }
+
+ return runningTotal;
+ };
+
+ /*
+ * This will return:
+ * 0 if the protocols are equal in priority (such that load order does not matter)
+ * < 0 if A is more important than B
+ * > 0 if B is more important than A
+ */
+ return protocolInjectionPriority(protoB) - protocolInjectionPriority(protoA);
+ });
+
+ unsigned classCount = objc_getClassList(NULL, 0);
+ if (!classCount) {
+ fprintf(stderr, "ERROR: No classes registered with the runtime\n");
+ return;
+ }
+
+ Class *allClasses = (Class *)malloc(sizeof(Class) * (classCount + 1));
+ if (!allClasses) {
+ fprintf(stderr, "ERROR: Could not allocate space for %u classes\n", classCount);
+ return;
+ }
+
+ // use this instead of mtl_copyClassList() to avoid sending +initialize to
+ // classes that we don't plan to inject into (this avoids some SenTestingKit
+ // timing issues)
+ classCount = objc_getClassList(allClasses, classCount);
+
+ /*
+ * set up an autorelease pool in case any Cocoa classes get used during
+ * the injection process or +initialize
+ */
+ @autoreleasepool {
+ // loop through the special protocols, and apply each one to all the
+ // classes in turn
+ //
+ // ORDER IS IMPORTANT HERE: protocols have to be injected to all classes in
+ // the order in which they appear in specialProtocols. Consider classes
+ // X and Y that implement protocols A and B, respectively. B needs to get
+ // its implementation into Y before A gets into X.
+ for (size_t i = 0;i < specialProtocolCount;++i) {
+ Protocol *protocol = specialProtocols[i].protocol;
+
+ // transfer ownership of the injection block to ARC and remove it
+ // from the structure
+ mtl_specialProtocolInjectionBlock injectionBlock = (__bridge_transfer id)specialProtocols[i].injectionBlock;
+ specialProtocols[i].injectionBlock = NULL;
+
+ // loop through all classes
+ for (unsigned classIndex = 0;classIndex < classCount;++classIndex) {
+ Class class = allClasses[classIndex];
+
+ // if this class doesn't conform to the protocol, continue to the
+ // next class immediately
+ if (!class_conformsToProtocol(class, protocol))
+ continue;
+
+ injectionBlock(class);
+ }
+ }
+ }
+
+ // free the allocated class list
+ free(allClasses);
+
+ // now that everything's injected, the special protocol list can also be
+ // destroyed
+ free(specialProtocols); specialProtocols = NULL;
+ specialProtocolCount = 0;
+ specialProtocolCapacity = 0;
+ specialProtocolsReady = 0;
+}
+
+unsigned mtl_injectMethods (
+ Class aClass,
+ Method *methods,
+ unsigned count,
+ mtl_methodInjectionBehavior behavior,
+ mtl_failedMethodCallback failedToAddCallback
+) {
+ unsigned successes = 0;
+
+ /*
+ * set up an autorelease pool in case any Cocoa classes invoke +initialize
+ * during this process
+ */
+ @autoreleasepool {
+ BOOL isMeta = class_isMetaClass(aClass);
+
+ if (!isMeta) {
+ // clear any +load and +initialize ignore flags
+ behavior &= ~(mtl_methodInjectionIgnoreLoad | mtl_methodInjectionIgnoreInitialize);
+ }
+
+ for (unsigned methodIndex = 0;methodIndex < count;++methodIndex) {
+ Method method = methods[methodIndex];
+ SEL methodName = method_getName(method);
+
+ if (behavior & mtl_methodInjectionIgnoreLoad) {
+ if (methodName == @selector(load)) {
+ ++successes;
+ continue;
+ }
+ }
+
+ if (behavior & mtl_methodInjectionIgnoreInitialize) {
+ if (methodName == @selector(initialize)) {
+ ++successes;
+ continue;
+ }
+ }
+
+ BOOL success = YES;
+ IMP impl = method_getImplementation(method);
+ const char *type = method_getTypeEncoding(method);
+
+ switch (behavior & mtl_methodInjectionOverwriteBehaviorMask) {
+ case mtl_methodInjectionFailOnExisting:
+ success = class_addMethod(aClass, methodName, impl, type);
+ break;
+
+ case mtl_methodInjectionFailOnAnyExisting:
+ if (class_getInstanceMethod(aClass, methodName)) {
+ success = NO;
+ break;
+ }
+
+ // else fall through
+
+ case mtl_methodInjectionReplace:
+ class_replaceMethod(aClass, methodName, impl, type);
+ break;
+
+ case mtl_methodInjectionFailOnSuperclassExisting:
+ {
+ Class superclass = class_getSuperclass(aClass);
+ if (superclass && class_getInstanceMethod(superclass, methodName))
+ success = NO;
+ else
+ class_replaceMethod(aClass, methodName, impl, type);
+ }
+
+ break;
+
+ default:
+ fprintf(stderr, "ERROR: Unrecognized method injection behavior: %i\n", (int)(behavior & mtl_methodInjectionOverwriteBehaviorMask));
+ }
+
+ if (success)
+ ++successes;
+ else
+ failedToAddCallback(aClass, method);
+ }
+ }
+
+ return successes;
+}
+
+BOOL mtl_injectMethodsFromClass (
+ Class srcClass,
+ Class dstClass,
+ mtl_methodInjectionBehavior behavior,
+ mtl_failedMethodCallback failedToAddCallback)
+{
+ unsigned count, addedCount;
+ BOOL success = YES;
+
+ count = 0;
+ Method *instanceMethods = class_copyMethodList(srcClass, &count);
+
+ addedCount = mtl_injectMethods(
+ dstClass,
+ instanceMethods,
+ count,
+ behavior,
+ failedToAddCallback
+ );
+
+ free(instanceMethods);
+ if (addedCount < count)
+ success = NO;
+
+ count = 0;
+ Method *classMethods = class_copyMethodList(object_getClass(srcClass), &count);
+
+ // ignore +load
+ behavior |= mtl_methodInjectionIgnoreLoad;
+ addedCount = mtl_injectMethods(
+ object_getClass(dstClass),
+ classMethods,
+ count,
+ behavior,
+ failedToAddCallback
+ );
+
+ free(classMethods);
+ if (addedCount < count)
+ success = NO;
+
+ return success;
+}
+
+Class mtl_classBeforeSuperclass (Class receiver, Class superclass) {
+ Class previousClass = nil;
+
+ while (![receiver isEqual:superclass]) {
+ previousClass = receiver;
+ receiver = class_getSuperclass(receiver);
+ }
+
+ return previousClass;
+}
+
+Class *mtl_copyClassList (unsigned *count) {
+ // get the number of classes registered with the runtime
+ int classCount = objc_getClassList(NULL, 0);
+ if (!classCount) {
+ if (count)
+ *count = 0;
+
+ return NULL;
+ }
+
+ // allocate space for them plus NULL
+ Class *allClasses = (Class *)malloc(sizeof(Class) * (classCount + 1));
+ if (!allClasses) {
+ fprintf(stderr, "ERROR: Could allocate memory for all classes\n");
+ if (count)
+ *count = 0;
+
+ return NULL;
+ }
+
+ // and then actually pull the list of the class objects
+ classCount = objc_getClassList(allClasses, classCount);
+ allClasses[classCount] = NULL;
+
+ @autoreleasepool {
+ // weed out classes that do weird things when reflected upon
+ for (int i = 0;i < classCount;) {
+ Class class = allClasses[i];
+ BOOL keep = YES;
+
+ if (keep)
+ keep &= class_respondsToSelector(class, @selector(methodSignatureForSelector:));
+
+ if (keep) {
+ if (class_respondsToSelector(class, @selector(isProxy)))
+ keep &= ![class isProxy];
+ }
+
+ if (!keep) {
+ if (--classCount > i) {
+ memmove(allClasses + i, allClasses + i + 1, (classCount - i) * sizeof(*allClasses));
+ }
+
+ continue;
+ }
+
+ ++i;
+ }
+ }
+
+ if (count)
+ *count = (unsigned)classCount;
+
+ return allClasses;
+}
+
+unsigned mtl_addMethods (Class aClass, Method *methods, unsigned count, BOOL checkSuperclasses, mtl_failedMethodCallback failedToAddCallback) {
+ mtl_methodInjectionBehavior behavior = mtl_methodInjectionFailOnExisting;
+ if (checkSuperclasses)
+ behavior |= mtl_methodInjectionFailOnSuperclassExisting;
+
+ return mtl_injectMethods(
+ aClass,
+ methods,
+ count,
+ behavior,
+ failedToAddCallback
+ );
+}
+
+BOOL mtl_addMethodsFromClass (Class srcClass, Class dstClass, BOOL checkSuperclasses, mtl_failedMethodCallback failedToAddCallback) {
+ mtl_methodInjectionBehavior behavior = mtl_methodInjectionFailOnExisting;
+ if (checkSuperclasses)
+ behavior |= mtl_methodInjectionFailOnSuperclassExisting;
+
+ return mtl_injectMethodsFromClass(srcClass, dstClass, behavior, failedToAddCallback);
+}
+
+BOOL mtl_classIsKindOfClass (Class receiver, Class aClass) {
+ while (receiver) {
+ if (receiver == aClass)
+ return YES;
+
+ receiver = class_getSuperclass(receiver);
+ }
+
+ return NO;
+}
+
+Class *mtl_copyClassListConformingToProtocol (Protocol *protocol, unsigned *count) {
+ Class *allClasses;
+
+ /*
+ * set up an autorelease pool in case any Cocoa classes invoke +initialize
+ * during this process
+ */
+ @autoreleasepool {
+ unsigned classCount = 0;
+ allClasses = mtl_copyClassList(&classCount);
+
+ if (!allClasses)
+ return NULL;
+
+ // we're going to reuse allClasses for the return value, so returnIndex will
+ // keep track of the indices we replace with new values
+ unsigned returnIndex = 0;
+
+ for (unsigned classIndex = 0;classIndex < classCount;++classIndex) {
+ Class cls = allClasses[classIndex];
+ if (class_conformsToProtocol(cls, protocol))
+ allClasses[returnIndex++] = cls;
+ }
+
+ allClasses[returnIndex] = NULL;
+ if (count)
+ *count = returnIndex;
+ }
+
+ return allClasses;
+}
+
+mtl_propertyAttributes *mtl_copyPropertyAttributes (objc_property_t property) {
+ const char * const attrString = property_getAttributes(property);
+ if (!attrString) {
+ fprintf(stderr, "ERROR: Could not get attribute string from property %s\n", property_getName(property));
+ return NULL;
+ }
+
+ if (attrString[0] != 'T') {
+ fprintf(stderr, "ERROR: Expected attribute string \"%s\" for property %s to start with 'T'\n", attrString, property_getName(property));
+ return NULL;
+ }
+
+ const char *typeString = attrString + 1;
+ const char *next = NSGetSizeAndAlignment(typeString, NULL, NULL);
+ if (!next) {
+ fprintf(stderr, "ERROR: Could not read past type in attribute string \"%s\" for property %s\n", attrString, property_getName(property));
+ return NULL;
+ }
+
+ size_t typeLength = next - typeString;
+ if (!typeLength) {
+ fprintf(stderr, "ERROR: Invalid type in attribute string \"%s\" for property %s\n", attrString, property_getName(property));
+ return NULL;
+ }
+
+ // allocate enough space for the structure and the type string (plus a NUL)
+ mtl_propertyAttributes *attributes = calloc(1, sizeof(mtl_propertyAttributes) + typeLength + 1);
+ if (!attributes) {
+ fprintf(stderr, "ERROR: Could not allocate mtl_propertyAttributes structure for attribute string \"%s\" for property %s\n", attrString, property_getName(property));
+ return NULL;
+ }
+
+ // copy the type string
+ strncpy(attributes->type, typeString, typeLength);
+ attributes->type[typeLength] = '\0';
+
+ // if this is an object type, and immediately followed by a quoted string...
+ if (typeString[0] == *(@encode(id)) && typeString[1] == '"') {
+ // we should be able to extract a class name
+ const char *className = typeString + 2;
+ next = strchr(className, '"');
+
+ if (!next) {
+ fprintf(stderr, "ERROR: Could not read class name in attribute string \"%s\" for property %s\n", attrString, property_getName(property));
+ goto errorOut;
+ }
+
+ if (className != next) {
+ size_t classNameLength = next - className;
+ char trimmedName[classNameLength + 1];
+
+ strncpy(trimmedName, className, classNameLength);
+ trimmedName[classNameLength] = '\0';
+
+ // attempt to look up the class in the runtime
+ attributes->objectClass = objc_getClass(trimmedName);
+ }
+ }
+
+ if (*next != '\0') {
+ // skip past any junk before the first flag
+ next = strchr(next, ',');
+ }
+
+ while (next && *next == ',') {
+ char flag = next[1];
+ next += 2;
+
+ switch (flag) {
+ case '\0':
+ break;
+
+ case 'R':
+ attributes->readonly = YES;
+ break;
+
+ case 'C':
+ attributes->memoryManagementPolicy = mtl_propertyMemoryManagementPolicyCopy;
+ break;
+
+ case '&':
+ attributes->memoryManagementPolicy = mtl_propertyMemoryManagementPolicyRetain;
+ break;
+
+ case 'N':
+ attributes->nonatomic = YES;
+ break;
+
+ case 'G':
+ case 'S':
+ {
+ const char *nextFlag = strchr(next, ',');
+ SEL name = NULL;
+
+ if (!nextFlag) {
+ // assume that the rest of the string is the selector
+ const char *selectorString = next;
+ next = "";
+
+ name = sel_registerName(selectorString);
+ } else {
+ size_t selectorLength = nextFlag - next;
+ if (!selectorLength) {
+ fprintf(stderr, "ERROR: Found zero length selector name in attribute string \"%s\" for property %s\n", attrString, property_getName(property));
+ goto errorOut;
+ }
+
+ char selectorString[selectorLength + 1];
+
+ strncpy(selectorString, next, selectorLength);
+ selectorString[selectorLength] = '\0';
+
+ name = sel_registerName(selectorString);
+ next = nextFlag;
+ }
+
+ if (flag == 'G')
+ attributes->getter = name;
+ else
+ attributes->setter = name;
+ }
+
+ break;
+
+ case 'D':
+ attributes->dynamic = YES;
+ attributes->ivar = NULL;
+ break;
+
+ case 'V':
+ // assume that the rest of the string (if present) is the ivar name
+ if (*next == '\0') {
+ // if there's nothing there, let's assume this is dynamic
+ attributes->ivar = NULL;
+ } else {
+ attributes->ivar = next;
+ next = "";
+ }
+
+ break;
+
+ case 'W':
+ attributes->weak = YES;
+ break;
+
+ case 'P':
+ attributes->canBeCollected = YES;
+ break;
+
+ case 't':
+ fprintf(stderr, "ERROR: Old-style type encoding is unsupported in attribute string \"%s\" for property %s\n", attrString, property_getName(property));
+
+ // skip over this type encoding
+ while (*next != ',' && *next != '\0')
+ ++next;
+
+ break;
+
+ default:
+ fprintf(stderr, "ERROR: Unrecognized attribute string flag '%c' in attribute string \"%s\" for property %s\n", flag, attrString, property_getName(property));
+ }
+ }
+
+ if (next && *next != '\0') {
+ fprintf(stderr, "Warning: Unparsed data \"%s\" in attribute string \"%s\" for property %s\n", next, attrString, property_getName(property));
+ }
+
+ if (!attributes->getter) {
+ // use the property name as the getter by default
+ attributes->getter = sel_registerName(property_getName(property));
+ }
+
+ if (!attributes->setter) {
+ const char *propertyName = property_getName(property);
+ size_t propertyNameLength = strlen(propertyName);
+
+ // we want to transform the name to setProperty: style
+ size_t setterLength = propertyNameLength + 4;
+
+ char setterName[setterLength + 1];
+ strncpy(setterName, "set", 3);
+ strncpy(setterName + 3, propertyName, propertyNameLength);
+
+ // capitalize property name for the setter
+ setterName[3] = (char)toupper(setterName[3]);
+
+ setterName[setterLength - 1] = ':';
+ setterName[setterLength] = '\0';
+
+ attributes->setter = sel_registerName(setterName);
+ }
+
+ return attributes;
+
+errorOut:
+ free(attributes);
+ return NULL;
+}
+
+Class *mtl_copySubclassList (Class targetClass, unsigned *subclassCount) {
+ unsigned classCount = 0;
+ Class *allClasses = mtl_copyClassList(&classCount);
+ if (!allClasses || !classCount) {
+ fprintf(stderr, "ERROR: No classes registered with the runtime, cannot find %s!\n", class_getName(targetClass));
+ return NULL;
+ }
+
+ // we're going to reuse allClasses for the return value, so returnIndex will
+ // keep track of the indices we replace with new values
+ unsigned returnIndex = 0;
+
+ BOOL isMeta = class_isMetaClass(targetClass);
+
+ for (unsigned classIndex = 0;classIndex < classCount;++classIndex) {
+ Class cls = allClasses[classIndex];
+ Class superclass = class_getSuperclass(cls);
+
+ while (superclass != NULL) {
+ if (isMeta) {
+ if (object_getClass(superclass) == targetClass)
+ break;
+ } else if (superclass == targetClass)
+ break;
+
+ superclass = class_getSuperclass(superclass);
+ }
+
+ if (!superclass)
+ continue;
+
+ // at this point, 'cls' is definitively a subclass of targetClass
+ if (isMeta)
+ cls = object_getClass(cls);
+
+ allClasses[returnIndex++] = cls;
+ }
+
+ allClasses[returnIndex] = NULL;
+ if (subclassCount)
+ *subclassCount = returnIndex;
+
+ return allClasses;
+}
+
+Method mtl_getImmediateInstanceMethod (Class aClass, SEL aSelector) {
+ unsigned methodCount = 0;
+ Method *methods = class_copyMethodList(aClass, &methodCount);
+ Method foundMethod = NULL;
+
+ for (unsigned methodIndex = 0;methodIndex < methodCount;++methodIndex) {
+ if (method_getName(methods[methodIndex]) == aSelector) {
+ foundMethod = methods[methodIndex];
+ break;
+ }
+ }
+
+ free(methods);
+ return foundMethod;
+}
+
+BOOL mtl_getPropertyAccessorsForClass (objc_property_t property, Class aClass, Method *getter, Method *setter) {
+ mtl_propertyAttributes *attributes = mtl_copyPropertyAttributes(property);
+ if (!attributes)
+ return NO;
+
+ SEL getterName = attributes->getter;
+ SEL setterName = attributes->setter;
+
+ free(attributes);
+ attributes = NULL;
+
+ /*
+ * set up an autorelease pool in case this sends aClass its first message
+ */
+ @autoreleasepool {
+ Method foundGetter = class_getInstanceMethod(aClass, getterName);
+ if (!foundGetter) {
+ return NO;
+ }
+
+ if (getter)
+ *getter = foundGetter;
+
+ if (setter) {
+ Method foundSetter = class_getInstanceMethod(aClass, setterName);
+ if (foundSetter)
+ *setter = foundSetter;
+ }
+ }
+
+ return YES;
+}
+
+BOOL mtl_loadSpecialProtocol (Protocol *protocol, void (^injectionBehavior)(Class destinationClass)) {
+ @autoreleasepool {
+ NSCParameterAssert(protocol != nil);
+ NSCParameterAssert(injectionBehavior != nil);
+
+ // lock the mutex to prevent accesses from other threads while we perform
+ // this work
+ if (pthread_mutex_lock(&specialProtocolsLock) != 0) {
+ fprintf(stderr, "ERROR: Could not synchronize on special protocol data\n");
+ return NO;
+ }
+
+ // if we've hit the hard maximum for number of special protocols, we can't
+ // continue
+ if (specialProtocolCount == SIZE_MAX) {
+ pthread_mutex_unlock(&specialProtocolsLock);
+ return NO;
+ }
+
+ // if the array has no more space, we will need to allocate additional
+ // entries
+ if (specialProtocolCount >= specialProtocolCapacity) {
+ size_t newCapacity;
+ if (specialProtocolCapacity == 0)
+ // if there are no entries, make space for just one
+ newCapacity = 1;
+ else {
+ // otherwise, double the current capacity
+ newCapacity = specialProtocolCapacity << 1;
+
+ // if the new capacity is less than the current capacity, that's
+ // unsigned integer overflow
+ if (newCapacity < specialProtocolCapacity) {
+ // set it to the maximum possible instead
+ newCapacity = SIZE_MAX;
+
+ // if the new capacity is still not greater than the current
+ // (for instance, if it was already SIZE_MAX), we can't continue
+ if (newCapacity <= specialProtocolCapacity) {
+ pthread_mutex_unlock(&specialProtocolsLock);
+ return NO;
+ }
+ }
+ }
+
+ // we have a new capacity, so resize the list of all special protocols
+ // to add the new entries
+ void * restrict ptr = realloc(specialProtocols, sizeof(*specialProtocols) * newCapacity);
+ if (!ptr) {
+ // the allocation failed, abort
+ pthread_mutex_unlock(&specialProtocolsLock);
+ return NO;
+ }
+
+ specialProtocols = ptr;
+ specialProtocolCapacity = newCapacity;
+ }
+
+ // at this point, there absolutely must be at least one empty entry in the
+ // array
+ assert(specialProtocolCount < specialProtocolCapacity);
+
+ // disable warning about "leaking" this block, which is released in
+ // mtl_injectSpecialProtocols()
+ #ifndef __clang_analyzer__
+ mtl_specialProtocolInjectionBlock copiedBlock = [injectionBehavior copy];
+
+ // construct a new MTLSpecialProtocol structure and add it to the first
+ // empty space in the array
+ specialProtocols[specialProtocolCount] = (MTLSpecialProtocol){
+ .protocol = protocol,
+ .injectionBlock = (__bridge_retained void *)copiedBlock,
+ .ready = NO
+ };
+ #endif
+
+ ++specialProtocolCount;
+ pthread_mutex_unlock(&specialProtocolsLock);
+ }
+
+ // success!
+ return YES;
+}
+
+void mtl_specialProtocolReadyForInjection (Protocol *protocol) {
+ @autoreleasepool {
+ NSCParameterAssert(protocol != nil);
+
+ // lock the mutex to prevent accesses from other threads while we perform
+ // this work
+ if (pthread_mutex_lock(&specialProtocolsLock) != 0) {
+ fprintf(stderr, "ERROR: Could not synchronize on special protocol data\n");
+ return;
+ }
+
+ // loop through all the special protocols in our list, trying to find the
+ // one associated with 'protocol'
+ for (size_t i = 0;i < specialProtocolCount;++i) {
+ if (specialProtocols[i].protocol == protocol) {
+ // found the matching special protocol, check to see if it's
+ // already ready
+ if (!specialProtocols[i].ready) {
+ // if it's not, mark it as being ready now
+ specialProtocols[i].ready = YES;
+
+ // since this special protocol was in our array, and it was not
+ // loaded, the total number of protocols loaded must be less
+ // than the total count at this point in time
+ assert(specialProtocolsReady < specialProtocolCount);
+
+ // ... and then increment the total number of special protocols
+ // loaded – if it now matches the total count of special
+ // protocols, begin the injection process
+ if (++specialProtocolsReady == specialProtocolCount)
+ mtl_injectSpecialProtocols();
+ }
+
+ break;
+ }
+ }
+
+ pthread_mutex_unlock(&specialProtocolsLock);
+ }
+}
+
+void mtl_removeMethod (Class aClass, SEL methodName) {
+ Method existingMethod = mtl_getImmediateInstanceMethod(aClass, methodName);
+ if (!existingMethod) {
+ return;
+ }
+
+ /*
+ * set up an autorelease pool in case any Cocoa classes invoke +initialize
+ * during this process
+ */
+ @autoreleasepool {
+ Method superclassMethod = NULL;
+ Class superclass = class_getSuperclass(aClass);
+ if (superclass)
+ superclassMethod = class_getInstanceMethod(superclass, methodName);
+
+ if (superclassMethod) {
+ method_setImplementation(existingMethod, method_getImplementation(superclassMethod));
+ } else {
+ // since we now know that the method doesn't exist on any
+ // superclass, get an IMP internal to the runtime for message forwarding
+ IMP forward = class_getMethodImplementation(superclass, methodName);
+
+ method_setImplementation(existingMethod, forward);
+ }
+ }
+}
+
+void mtl_replaceMethods (Class aClass, Method *methods, unsigned count) {
+ mtl_injectMethods(
+ aClass,
+ methods,
+ count,
+ mtl_methodInjectionReplace,
+ NULL
+ );
+}
+
+void mtl_replaceMethodsFromClass (Class srcClass, Class dstClass) {
+ mtl_injectMethodsFromClass(srcClass, dstClass, mtl_methodInjectionReplace, NULL);
+}
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wformat"
+NSString *mtl_stringFromTypedBytes (const void *bytes, const char *encoding) {
+ switch (*encoding) {
+ case 'c': return @(*(char *)bytes).description;
+ case 'C': return @(*(unsigned char *)bytes).description;
+ case 'i': return @(*(int *)bytes).description;
+ case 'I': return @(*(unsigned int *)bytes).description;
+ case 's': return @(*(short *)bytes).description;
+ case 'S': return @(*(unsigned short *)bytes).description;
+ case 'l': return @(*(long *)bytes).description;
+ case 'L': return @(*(unsigned long *)bytes).description;
+ case 'q': return @(*(long long *)bytes).description;
+ case 'Q': return @(*(unsigned long long *)bytes).description;
+ case 'f': return @(*(float *)bytes).description;
+ case 'd': return @(*(double *)bytes).description;
+ case 'B': return @(*(_Bool *)bytes).description;
+ case 'v': return @"(void)";
+ case '*': return [NSString stringWithFormat:@"\"%s\"", bytes];
+
+ case '@':
+ case '#': {
+ id obj = *(__unsafe_unretained id *)bytes;
+ if (obj)
+ return [obj description];
+ else
+ return @"(nil)";
+ }
+
+ case '?':
+ case '^': {
+ const void *ptr = *(const void **)bytes;
+ if (ptr)
+ return [NSString stringWithFormat:@"%p", ptr];
+ else
+ return @"(null)";
+ }
+
+ default:
+ return [[NSValue valueWithBytes:bytes objCType:encoding] description];
+ }
+}
+#pragma clang diagnostic pop
diff --git a/Apps/Wikipedia/WMF Framework/Third Party/Mantle/extobjc/MTLEXTScope.m b/Apps/Wikipedia/WMF Framework/Third Party/Mantle/extobjc/MTLEXTScope.m
new file mode 100644
index 0000000..60cf275
--- /dev/null
+++ b/Apps/Wikipedia/WMF Framework/Third Party/Mantle/extobjc/MTLEXTScope.m
@@ -0,0 +1,15 @@
+//
+// MTLEXTScope.m
+// extobjc
+//
+// Created by Justin Spahr-Summers on 2011-05-04.
+// Copyright (C) 2012 Justin Spahr-Summers.
+// Released under the MIT license.
+//
+
+#import "MTLEXTScope.h"
+
+void mtl_executeCleanupBlock (__strong mtl_cleanupBlock_t *block) {
+ (*block)();
+}
+
diff --git a/Apps/Wikipedia/WMF Framework/Third Party/Mantle/extobjc/include/MTLEXTKeyPathCoding.h b/Apps/Wikipedia/WMF Framework/Third Party/Mantle/extobjc/include/MTLEXTKeyPathCoding.h
new file mode 100644
index 0000000..eea5457
--- /dev/null
+++ b/Apps/Wikipedia/WMF Framework/Third Party/Mantle/extobjc/include/MTLEXTKeyPathCoding.h
@@ -0,0 +1,77 @@
+//
+// MTLEXTKeyPathCoding.h
+// extobjc
+//
+// Created by Justin Spahr-Summers on 19.06.12.
+// Copyright (C) 2012 Justin Spahr-Summers.
+// Released under the MIT license.
+//
+
+#import
+#import "MTLMetamacros.h"
+
+/**
+ * \@keypath allows compile-time verification of key paths. Given a real object
+ * receiver and key path:
+ *
+ * @code
+
+NSString *UTF8StringPath = @keypath(str.lowercaseString.UTF8String);
+// => @"lowercaseString.UTF8String"
+
+NSString *versionPath = @keypath(NSObject, version);
+// => @"version"
+
+NSString *lowercaseStringPath = @keypath(NSString.new, lowercaseString);
+// => @"lowercaseString"
+
+ * @endcode
+ *
+ * ... the macro returns an \c NSString containing all but the first path
+ * component or argument (e.g., @"lowercaseString.UTF8String", @"version").
+ *
+ * In addition to simply creating a key path, this macro ensures that the key
+ * path is valid at compile-time (causing a syntax error if not), and supports
+ * refactoring, such that changing the name of the property will also update any
+ * uses of \@keypath.
+ */
+#define keypath(...) \
+ _Pragma("clang diagnostic push") \
+ _Pragma("clang diagnostic ignored \"-Warc-repeated-use-of-weak\"") \
+ (NO).boolValue ? ((NSString * _Nonnull)nil) : ((NSString * _Nonnull)@(cStringKeypath(__VA_ARGS__))) \
+ _Pragma("clang diagnostic pop") \
+
+#define cStringKeypath(...) \
+ metamacro_if_eq(1, metamacro_argcount(__VA_ARGS__))(keypath1(__VA_ARGS__))(keypath2(__VA_ARGS__))
+
+#define keypath1(PATH) \
+ (((void)(NO && ((void)PATH, NO)), \
+ ({ char *__extobjckeypath__ = strchr(# PATH, '.'); NSCAssert(__extobjckeypath__, @"Provided key path is invalid."); __extobjckeypath__ + 1; })))
+
+#define keypath2(OBJ, PATH) \
+ (((void)(NO && ((void)OBJ.PATH, NO)), # PATH))
+
+/**
+ * \@collectionKeypath allows compile-time verification of key paths across collections NSArray/NSSet etc. Given a real object
+ * receiver, collection object receiver and related keypaths:
+ *
+ * @code
+
+ NSString *employeesFirstNamePath = @collectionKeypath(department.employees, Employee.new, firstName)
+ // => @"employees.firstName"
+
+ NSString *employeesFirstNamePath = @collectionKeypath(Department.new, employees, Employee.new, firstName)
+ // => @"employees.firstName"
+
+ * @endcode
+ *
+ */
+#define collectionKeypath(...) \
+ metamacro_if_eq(3, metamacro_argcount(__VA_ARGS__))(collectionKeypath3(__VA_ARGS__))(collectionKeypath4(__VA_ARGS__))
+
+#define collectionKeypath3(PATH, COLLECTION_OBJECT, COLLECTION_PATH) \
+ (YES).boolValue ? (NSString * _Nonnull)@((const char * _Nonnull)[[NSString stringWithFormat:@"%s.%s", cStringKeypath(PATH), cStringKeypath(COLLECTION_OBJECT, COLLECTION_PATH)] UTF8String]) : (NSString * _Nonnull)nil
+
+#define collectionKeypath4(OBJ, PATH, COLLECTION_OBJECT, COLLECTION_PATH) \
+ (YES).boolValue ? (NSString * _Nonnull)@((const char * _Nonnull)[[NSString stringWithFormat:@"%s.%s", cStringKeypath(OBJ, PATH), cStringKeypath(COLLECTION_OBJECT, COLLECTION_PATH)] UTF8String]) : (NSString * _Nonnull)nil
+
diff --git a/Apps/Wikipedia/WMF Framework/Third Party/Mantle/extobjc/include/MTLEXTRuntimeExtensions.h b/Apps/Wikipedia/WMF Framework/Third Party/Mantle/extobjc/include/MTLEXTRuntimeExtensions.h
new file mode 100644
index 0000000..01f5c3c
--- /dev/null
+++ b/Apps/Wikipedia/WMF Framework/Third Party/Mantle/extobjc/include/MTLEXTRuntimeExtensions.h
@@ -0,0 +1,389 @@
+//
+// MTLEXTRuntimeExtensions.h
+// extobjc
+//
+// Created by Justin Spahr-Summers on 2011-03-05.
+// Copyright (C) 2012 Justin Spahr-Summers.
+// Released under the MIT license.
+//
+
+#import
+#import
+
+/**
+ * A callback indicating that the given method failed to be added to the given
+ * class. The reason for the failure depends on the attempted task.
+ */
+typedef void (*mtl_failedMethodCallback)(Class, Method);
+
+/**
+ * Used with #mtl_injectMethods to determine injection behavior.
+ */
+typedef NS_OPTIONS(NSUInteger, mtl_methodInjectionBehavior) {
+ /**
+ * Indicates that any existing methods on the destination class should be
+ * overwritten.
+ */
+ mtl_methodInjectionReplace = 0x00,
+
+ /**
+ * Avoid overwriting methods on the immediate destination class.
+ */
+ mtl_methodInjectionFailOnExisting = 0x01,
+
+ /**
+ * Avoid overriding methods implemented in any superclass of the destination
+ * class.
+ */
+ mtl_methodInjectionFailOnSuperclassExisting = 0x02,
+
+ /**
+ * Avoid overwriting methods implemented in the immediate destination class
+ * or any superclass. This is equivalent to
+ * mtl_methodInjectionFailOnExisting | mtl_methodInjectionFailOnSuperclassExisting.
+ */
+ mtl_methodInjectionFailOnAnyExisting = 0x03,
+
+ /**
+ * Ignore the \c +load class method. This does not affect instance method
+ * injection.
+ */
+ mtl_methodInjectionIgnoreLoad = 1U << 2,
+
+ /**
+ * Ignore the \c +initialize class method. This does not affect instance method
+ * injection.
+ */
+ mtl_methodInjectionIgnoreInitialize = 1U << 3
+};
+
+/**
+ * A mask for the overwriting behavior flags of #mtl_methodInjectionBehavior.
+ */
+static const mtl_methodInjectionBehavior mtl_methodInjectionOverwriteBehaviorMask = 0x3;
+
+/**
+ * Describes the memory management policy of a property.
+ */
+typedef enum {
+ /**
+ * The value is assigned.
+ */
+ mtl_propertyMemoryManagementPolicyAssign = 0,
+
+ /**
+ * The value is retained.
+ */
+ mtl_propertyMemoryManagementPolicyRetain,
+
+ /**
+ * The value is copied.
+ */
+ mtl_propertyMemoryManagementPolicyCopy
+} mtl_propertyMemoryManagementPolicy;
+
+/**
+ * Describes the attributes and type information of a property.
+ */
+typedef struct {
+ /**
+ * Whether this property was declared with the \c readonly attribute.
+ */
+ BOOL readonly;
+
+ /**
+ * Whether this property was declared with the \c nonatomic attribute.
+ */
+ BOOL nonatomic;
+
+ /**
+ * Whether the property is a weak reference.
+ */
+ BOOL weak;
+
+ /**
+ * Whether the property is eligible for garbage collection.
+ */
+ BOOL canBeCollected;
+
+ /**
+ * Whether this property is defined with \c \@dynamic.
+ */
+ BOOL dynamic;
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wpadded"
+
+ /**
+ * The memory management policy for this property. This will always be
+ * #mtl_propertyMemoryManagementPolicyAssign if #readonly is \c YES.
+ */
+ mtl_propertyMemoryManagementPolicy memoryManagementPolicy;
+
+
+ /**
+ * The selector for the getter of this property. This will reflect any
+ * custom \c getter= attribute provided in the property declaration, or the
+ * inferred getter name otherwise.
+ */
+ SEL getter;
+
+ /**
+ * The selector for the setter of this property. This will reflect any
+ * custom \c setter= attribute provided in the property declaration, or the
+ * inferred setter name otherwise.
+ *
+ * @note If #readonly is \c YES, this value will represent what the setter
+ * \e would be, if the property were writable.
+ */
+ SEL setter;
+
+#pragma clang diagnostic pop
+
+ /**
+ * The backing instance variable for this property, or \c NULL if \c
+ * \c @synthesize was not used, and therefore no instance variable exists. This
+ * would also be the case if the property is implemented dynamically.
+ */
+ const char *ivar;
+
+ /**
+ * If this property is defined as being an instance of a specific class,
+ * this will be the class object representing it.
+ *
+ * This will be \c nil if the property was defined as type \c id, if the
+ * property is not of an object type, or if the class could not be found at
+ * runtime.
+ */
+ Class objectClass;
+
+ /**
+ * The type encoding for the value of this property. This is the type as it
+ * would be returned by the \c \@encode() directive.
+ */
+ char type[];
+} mtl_propertyAttributes;
+
+/**
+ * Iterates through the first \a count entries in \a methods and attempts to add
+ * each one to \a aClass. If a method by the same name already exists on \a
+ * aClass, it is \e not overridden. If \a checkSuperclasses is \c YES, and
+ * a method by the same name already exists on any superclass of \a aClass, it
+ * is not overridden.
+ *
+ * Returns the number of methods added successfully. For each method that fails
+ * to be added, \a failedToAddCallback (if provided) is invoked.
+ */
+unsigned mtl_addMethods (Class aClass, Method *methods, unsigned count, BOOL checkSuperclasses, mtl_failedMethodCallback failedToAddCallback);
+
+/**
+ * Iterates through all instance and class methods of \a srcClass and attempts
+ * to add each one to \a dstClass. If a method by the same name already exists
+ * on \a aClass, it is \e not overridden. If \a checkSuperclasses is \c YES, and
+ * a method by the same name already exists on any superclass of \a aClass, it
+ * is not overridden.
+ *
+ * Returns whether all methods were added successfully. For each method that fails
+ * to be added, \a failedToAddCallback (if provided) is invoked.
+ *
+ * @note This ignores any \c +load method on \a srcClass. \a srcClass and \a
+ * dstClass must not be metaclasses.
+ */
+BOOL mtl_addMethodsFromClass (Class srcClass, Class dstClass, BOOL checkSuperclasses, mtl_failedMethodCallback failedToAddCallback);
+
+/**
+ * Returns the superclass of \a receiver which immediately descends from \a
+ * superclass. If \a superclass is not in the hierarchy of \a receiver, or is
+ * equal to \a receiver, \c nil is returned.
+ */
+Class mtl_classBeforeSuperclass (Class receiver, Class superclass);
+
+/**
+ * Returns whether \a receiver is \a aClass, or inherits directly from it.
+ */
+BOOL mtl_classIsKindOfClass (Class receiver, Class aClass);
+
+/**
+ * Returns the full list of classes registered with the runtime, terminated with
+ * \c NULL. If \a count is not \c NULL, it is filled in with the total number of
+ * classes returned. You must \c free() the returned array.
+ */
+Class *mtl_copyClassList (unsigned *count);
+
+/**
+ * Looks through the complete list of classes registered with the runtime and
+ * finds all classes which conform to \a protocol. Returns \c *count classes
+ * terminated by a \c NULL. You must \c free() the returned array. If there are no
+ * classes conforming to \a protocol, \c NULL is returned.
+ *
+ * @note \a count may be \c NULL.
+ */
+Class *mtl_copyClassListConformingToProtocol (Protocol *protocol, unsigned *count);
+
+/**
+ * Returns a pointer to a structure containing information about \a property.
+ * You must \c free() the returned pointer. Returns \c NULL if there is an error
+ * obtaining information from \a property.
+ */
+mtl_propertyAttributes *mtl_copyPropertyAttributes (objc_property_t property);
+
+/**
+ * Looks through the complete list of classes registered with the runtime and
+ * finds all classes which are descendant from \a aClass. Returns \c
+ * *subclassCount classes terminated by a \c NULL. You must \c free() the
+ * returned array. If there are no subclasses of \a aClass, \c NULL is
+ * returned.
+ *
+ * @note \a subclassCount may be \c NULL. \a aClass may be a metaclass to get
+ * all subclass metaclass objects.
+ */
+Class *mtl_copySubclassList (Class aClass, unsigned *subclassCount);
+
+/**
+ * Finds the instance method named \a aSelector on \a aClass and returns it, or
+ * returns \c NULL if no such instance method exists. Unlike \c
+ * class_getInstanceMethod(), this does not search superclasses.
+ *
+ * @note To get class methods in this manner, use a metaclass for \a aClass.
+ */
+Method mtl_getImmediateInstanceMethod (Class aClass, SEL aSelector);
+
+/**
+ * Returns the value of \c Ivar \a IVAR from instance \a OBJ. The instance
+ * variable must be of type \a TYPE, and is returned as such.
+ *
+ * @warning Depending on the platform, this may or may not work with aggregate
+ * or floating-point types.
+ */
+#define mtl_getIvar(OBJ, IVAR, TYPE) \
+ ((TYPE (*)(id, Ivar)object_getIvar)((OBJ), (IVAR)))
+
+/**
+ * Returns the value of the instance variable identified by the string \a NAME
+ * from instance \a OBJ. The instance variable must be of type \a TYPE, and is
+ * returned as such.
+ *
+ * @note \a OBJ is evaluated twice.
+ *
+ * @warning Depending on the platform, this may or may not work with aggregate
+ * or floating-point types.
+ */
+#define mtl_getIvarByName(OBJ, NAME, TYPE) \
+ mtl_getIvar((OBJ), class_getInstanceVariable(object_getClass((OBJ)), (NAME)), TYPE)
+
+/**
+ * Returns the accessor methods for \a property, as implemented in \a aClass or
+ * any of its superclasses. The getter, if implemented, is returned in \a
+ * getter, and the setter, if implemented, is returned in \a setter. If either
+ * \a getter or \a setter are \c NULL, that accessor is not returned. If either
+ * accessor is not implemented, the argument is left unmodified.
+ *
+ * Returns \c YES if a valid accessor was found, or \c NO if \a aClass and its
+ * superclasses do not implement \a property or if an error occurs.
+ */
+BOOL mtl_getPropertyAccessorsForClass (objc_property_t property, Class aClass, Method *getter, Method *setter);
+
+/**
+ * For all classes registered with the runtime, invokes \c
+ * methodSignatureForSelector: and \c instanceMethodSignatureForSelector: to
+ * determine a method signature for \a aSelector. If one or more valid
+ * signatures is found, the first one is returned. If no valid signatures were
+ * found, \c nil is returned.
+ */
+NSMethodSignature *mtl_globalMethodSignatureForSelector (SEL aSelector);
+
+/**
+ * Highly-configurable method injection. Adds the first \a count entries from \a
+ * methods into \a aClass according to \a behavior.
+ *
+ * Returns the number of methods added successfully. For each method that fails
+ * to be added, \a failedToAddCallback (if provided) is invoked.
+ *
+ * @note \c +load and \c +initialize methods are included in the number of
+ * successful methods when ignored for injection.
+ */
+unsigned mtl_injectMethods (Class aClass, Method *methods, unsigned count, mtl_methodInjectionBehavior behavior, mtl_failedMethodCallback failedToAddCallback);
+
+/**
+ * Invokes #mtl_injectMethods with the instance methods and class methods from
+ * \a srcClass. #mtl_methodInjectionIgnoreLoad is added to #behavior for class
+ * method injection.
+ *
+ * Returns whether all methods were added successfully. For each method that fails
+ * to be added, \a failedToAddCallback (if provided) is invoked.
+ *
+ * @note \c +load and \c +initialize methods are considered to be added
+ * successfully when ignored for injection.
+ */
+BOOL mtl_injectMethodsFromClass (Class srcClass, Class dstClass, mtl_methodInjectionBehavior behavior, mtl_failedMethodCallback failedToAddCallback);
+
+/**
+ * Loads a "special protocol" into an internal list. A special protocol is any
+ * protocol for which implementing classes need injection behavior (i.e., any
+ * class conforming to the protocol needs to be reflected upon). Returns \c NO
+ * if loading failed.
+ *
+ * Using this facility proceeds as follows:
+ *
+ * @li Each protocol is loaded with #mtl_loadSpecialProtocol and a custom block
+ * that describes its injection behavior on each conforming class.
+ * @li Each protocol is marked as being ready for injection with
+ * #mtl_specialProtocolReadyForInjection.
+ * @li The entire Objective-C class list is retrieved, and each special
+ * protocol's \a injectionBehavior block is run for all conforming classes.
+ *
+ * It is an error to call this function without later calling
+ * #mtl_specialProtocolReadyForInjection as well.
+ *
+ * @note A special protocol X which conforms to another special protocol Y is
+ * always injected \e after Y.
+ */
+BOOL mtl_loadSpecialProtocol (Protocol *protocol, void (^injectionBehavior)(Class destinationClass));
+
+/**
+ * Marks a special protocol as being ready for injection. Injection is actually
+ * performed only after all special protocols have been marked in this way.
+ *
+ * @sa mtl_loadSpecialProtocol
+ */
+void mtl_specialProtocolReadyForInjection (Protocol *protocol);
+
+/**
+ * Creates a human-readable description of the data in \a bytes, interpreting it
+ * according to the given Objective-C type encoding.
+ *
+ * This is intended for use with debugging, and code should not depend upon the
+ * format of the returned string (just like a call to \c -description).
+ */
+NSString *mtl_stringFromTypedBytes (const void *bytes, const char *encoding);
+
+/**
+ * "Removes" any instance method matching \a methodName from \a aClass. This
+ * removal can mean one of two things:
+ *
+ * @li If any superclass of \a aClass implements a method by the same name, the
+ * implementation of the closest such superclass is used.
+ * @li If no superclasses of \a aClass implement a method by the same name, the
+ * method is replaced with an implementation internal to the runtime, used for
+ * message forwarding.
+ *
+ * @warning Adding a method by the same name into a superclass of \a aClass \e
+ * after using this function may obscure it from the subclass.
+ */
+void mtl_removeMethod (Class aClass, SEL methodName);
+
+/**
+ * Iterates through the first \a count entries in \a methods and adds each one
+ * to \a aClass, replacing any existing implementation.
+ */
+void mtl_replaceMethods (Class aClass, Method *methods, unsigned count);
+
+/**
+ * Iterates through all instance and class methods of \a srcClass and adds each
+ * one to \a dstClass, replacing any existing implementation.
+ *
+ * @note This ignores any \c +load method on \a srcClass. \a srcClass and \a
+ * dstClass must not be metaclasses.
+ */
+void mtl_replaceMethodsFromClass (Class srcClass, Class dstClass);
+
diff --git a/Apps/Wikipedia/WMF Framework/Third Party/Mantle/extobjc/include/MTLEXTScope.h b/Apps/Wikipedia/WMF Framework/Third Party/Mantle/extobjc/include/MTLEXTScope.h
new file mode 100644
index 0000000..815297c
--- /dev/null
+++ b/Apps/Wikipedia/WMF Framework/Third Party/Mantle/extobjc/include/MTLEXTScope.h
@@ -0,0 +1,122 @@
+//
+// MTLEXTScope.h
+// extobjc
+//
+// Created by Justin Spahr-Summers on 2011-05-04.
+// Copyright (C) 2012 Justin Spahr-Summers.
+// Released under the MIT license.
+//
+
+#import "MTLMetamacros.h"
+
+/**
+ * \@onExit defines some code to be executed when the current scope exits. The
+ * code must be enclosed in braces and terminated with a semicolon, and will be
+ * executed regardless of how the scope is exited, including from exceptions,
+ * \c goto, \c return, \c break, and \c continue.
+ *
+ * Provided code will go into a block to be executed later. Keep this in mind as
+ * it pertains to memory management, restrictions on assignment, etc. Because
+ * the code is used within a block, \c return is a legal (though perhaps
+ * confusing) way to exit the cleanup block early.
+ *
+ * Multiple \@onExit statements in the same scope are executed in reverse
+ * lexical order. This helps when pairing resource acquisition with \@onExit
+ * statements, as it guarantees teardown in the opposite order of acquisition.
+ *
+ * @note This statement cannot be used within scopes defined without braces
+ * (like a one line \c if). In practice, this is not an issue, since \@onExit is
+ * a useless construct in such a case anyways.
+ */
+#define onExit \
+ mtl_keywordify \
+ __strong mtl_cleanupBlock_t metamacro_concat(mtl_exitBlock_, __LINE__) __attribute__((cleanup(mtl_executeCleanupBlock), unused)) = ^
+
+/**
+ * Creates \c __weak shadow variables for each of the variables provided as
+ * arguments, which can later be made strong again with #strongify.
+ *
+ * This is typically used to weakly reference variables in a block, but then
+ * ensure that the variables stay alive during the actual execution of the block
+ * (if they were live upon entry).
+ *
+ * See #strongify for an example of usage.
+ */
+#define weakify(...) \
+ mtl_keywordify \
+ metamacro_foreach_cxt(mtl_weakify_,, __weak, __VA_ARGS__)
+
+/**
+ * Like #weakify, but uses \c __unsafe_unretained instead, for targets or
+ * classes that do not support weak references.
+ */
+#define unsafeify(...) \
+ mtl_keywordify \
+ metamacro_foreach_cxt(mtl_weakify_,, __unsafe_unretained, __VA_ARGS__)
+
+/**
+ * Strongly references each of the variables provided as arguments, which must
+ * have previously been passed to #weakify.
+ *
+ * The strong references created will shadow the original variable names, such
+ * that the original names can be used without issue (and a significantly
+ * reduced risk of retain cycles) in the current scope.
+ *
+ * @code
+
+ id foo = [[NSObject alloc] init];
+ id bar = [[NSObject alloc] init];
+
+ @weakify(foo, bar);
+
+ // this block will not keep 'foo' or 'bar' alive
+ BOOL (^matchesFooOrBar)(id) = ^ BOOL (id obj){
+ // but now, upon entry, 'foo' and 'bar' will stay alive until the block has
+ // finished executing
+ @strongify(foo, bar);
+
+ return [foo isEqual:obj] || [bar isEqual:obj];
+ };
+
+ * @endcode
+ */
+#define strongify(...) \
+ mtl_keywordify \
+ _Pragma("clang diagnostic push") \
+ _Pragma("clang diagnostic ignored \"-Wshadow\"") \
+ metamacro_foreach(mtl_strongify_,, __VA_ARGS__) \
+ _Pragma("clang diagnostic pop")
+
+/*** implementation details follow ***/
+typedef void (^mtl_cleanupBlock_t)(void);
+
+#if defined(__cplusplus)
+extern "C" {
+#endif
+ void mtl_executeCleanupBlock (__strong mtl_cleanupBlock_t *block);
+#if defined(__cplusplus)
+}
+#endif
+
+#define mtl_weakify_(INDEX, CONTEXT, VAR) \
+ CONTEXT __typeof__(VAR) metamacro_concat(VAR, _weak_) = (VAR);
+
+#define mtl_strongify_(INDEX, VAR) \
+ __strong __typeof__(VAR) VAR = metamacro_concat(VAR, _weak_);
+
+// Details about the choice of backing keyword:
+//
+// The use of @try/@catch/@finally can cause the compiler to suppress
+// return-type warnings.
+// The use of @autoreleasepool {} is not optimized away by the compiler,
+// resulting in superfluous creation of autorelease pools.
+//
+// Since neither option is perfect, and with no other alternatives, the
+// compromise is to use @autorelease in DEBUG builds to maintain compiler
+// analysis, and to use @try/@catch otherwise to avoid insertion of unnecessary
+// autorelease pools.
+#if defined(DEBUG) && !defined(NDEBUG)
+#define mtl_keywordify autoreleasepool {}
+#else
+#define mtl_keywordify try {} @catch (...) {}
+#endif
diff --git a/Apps/Wikipedia/WMF Framework/Third Party/Mantle/extobjc/include/MTLMetamacros.h b/Apps/Wikipedia/WMF Framework/Third Party/Mantle/extobjc/include/MTLMetamacros.h
new file mode 100644
index 0000000..dd90d99
--- /dev/null
+++ b/Apps/Wikipedia/WMF Framework/Third Party/Mantle/extobjc/include/MTLMetamacros.h
@@ -0,0 +1,667 @@
+/**
+ * Macros for metaprogramming
+ * ExtendedC
+ *
+ * Copyright (C) 2012 Justin Spahr-Summers
+ * Released under the MIT license
+ */
+
+#ifndef EXTC_METAMACROS_H
+#define EXTC_METAMACROS_H
+
+
+/**
+ * Executes one or more expressions (which may have a void type, such as a call
+ * to a function that returns no value) and always returns true.
+ */
+#define metamacro_exprify(...) \
+ ((__VA_ARGS__), true)
+
+/**
+ * Returns a string representation of VALUE after full macro expansion.
+ */
+#define metamacro_stringify(VALUE) \
+ metamacro_stringify_(VALUE)
+
+/**
+ * Returns A and B concatenated after full macro expansion.
+ */
+#define metamacro_concat(A, B) \
+ metamacro_concat_(A, B)
+
+/**
+ * Returns the Nth variadic argument (starting from zero). At least
+ * N + 1 variadic arguments must be given. N must be between zero and twenty,
+ * inclusive.
+ */
+#define metamacro_at(N, ...) \
+ metamacro_concat(metamacro_at, N)(__VA_ARGS__)
+
+/**
+ * Returns the number of arguments (up to twenty) provided to the macro. At
+ * least one argument must be provided.
+ *
+ * Inspired by P99: http://p99.gforge.inria.fr
+ */
+#define metamacro_argcount(...) \
+ metamacro_at(20, __VA_ARGS__, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1)
+
+/**
+ * Identical to #metamacro_foreach_cxt, except that no CONTEXT argument is
+ * given. Only the index and current argument will thus be passed to MACRO.
+ */
+#define metamacro_foreach(MACRO, SEP, ...) \
+ metamacro_foreach_cxt(metamacro_foreach_iter, SEP, MACRO, __VA_ARGS__)
+
+/**
+ * For each consecutive variadic argument (up to twenty), MACRO is passed the
+ * zero-based index of the current argument, CONTEXT, and then the argument
+ * itself. The results of adjoining invocations of MACRO are then separated by
+ * SEP.
+ *
+ * Inspired by P99: http://p99.gforge.inria.fr
+ */
+#define metamacro_foreach_cxt(MACRO, SEP, CONTEXT, ...) \
+ metamacro_concat(metamacro_foreach_cxt, metamacro_argcount(__VA_ARGS__))(MACRO, SEP, CONTEXT, __VA_ARGS__)
+
+/**
+ * Identical to #metamacro_foreach_cxt. This can be used when the former would
+ * fail due to recursive macro expansion.
+ */
+#define metamacro_foreach_cxt_recursive(MACRO, SEP, CONTEXT, ...) \
+ metamacro_concat(metamacro_foreach_cxt_recursive, metamacro_argcount(__VA_ARGS__))(MACRO, SEP, CONTEXT, __VA_ARGS__)
+
+/**
+ * In consecutive order, appends each variadic argument (up to twenty) onto
+ * BASE. The resulting concatenations are then separated by SEP.
+ *
+ * This is primarily useful to manipulate a list of macro invocations into instead
+ * invoking a different, possibly related macro.
+ */
+#define metamacro_foreach_concat(BASE, SEP, ...) \
+ metamacro_foreach_cxt(metamacro_foreach_concat_iter, SEP, BASE, __VA_ARGS__)
+
+/**
+ * Iterates COUNT times, each time invoking MACRO with the current index
+ * (starting at zero) and CONTEXT. The results of adjoining invocations of MACRO
+ * are then separated by SEP.
+ *
+ * COUNT must be an integer between zero and twenty, inclusive.
+ */
+#define metamacro_for_cxt(COUNT, MACRO, SEP, CONTEXT) \
+ metamacro_concat(metamacro_for_cxt, COUNT)(MACRO, SEP, CONTEXT)
+
+/**
+ * Returns the first argument given. At least one argument must be provided.
+ *
+ * This is useful when implementing a variadic macro, where you may have only
+ * one variadic argument, but no way to retrieve it (for example, because \c ...
+ * always needs to match at least one argument).
+ *
+ * @code
+
+#define varmacro(...) \
+ metamacro_head(__VA_ARGS__)
+
+ * @endcode
+ */
+#define metamacro_head(...) \
+ metamacro_head_(__VA_ARGS__, 0)
+
+/**
+ * Returns every argument except the first. At least two arguments must be
+ * provided.
+ */
+#define metamacro_tail(...) \
+ metamacro_tail_(__VA_ARGS__)
+
+/**
+ * Returns the first N (up to twenty) variadic arguments as a new argument list.
+ * At least N variadic arguments must be provided.
+ */
+#define metamacro_take(N, ...) \
+ metamacro_concat(metamacro_take, N)(__VA_ARGS__)
+
+/**
+ * Removes the first N (up to twenty) variadic arguments from the given argument
+ * list. At least N variadic arguments must be provided.
+ */
+#define metamacro_drop(N, ...) \
+ metamacro_concat(metamacro_drop, N)(__VA_ARGS__)
+
+/**
+ * Decrements VAL, which must be a number between zero and twenty, inclusive.
+ *
+ * This is primarily useful when dealing with indexes and counts in
+ * metaprogramming.
+ */
+#define metamacro_dec(VAL) \
+ metamacro_at(VAL, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19)
+
+/**
+ * Increments VAL, which must be a number between zero and twenty, inclusive.
+ *
+ * This is primarily useful when dealing with indexes and counts in
+ * metaprogramming.
+ */
+#define metamacro_inc(VAL) \
+ metamacro_at(VAL, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21)
+
+/**
+ * If A is equal to B, the next argument list is expanded; otherwise, the
+ * argument list after that is expanded. A and B must be numbers between zero
+ * and twenty, inclusive. Additionally, B must be greater than or equal to A.
+ *
+ * @code
+
+// expands to true
+metamacro_if_eq(0, 0)(true)(false)
+
+// expands to false
+metamacro_if_eq(0, 1)(true)(false)
+
+ * @endcode
+ *
+ * This is primarily useful when dealing with indexes and counts in
+ * metaprogramming.
+ */
+#define metamacro_if_eq(A, B) \
+ metamacro_concat(metamacro_if_eq, A)(B)
+
+/**
+ * Identical to #metamacro_if_eq. This can be used when the former would fail
+ * due to recursive macro expansion.
+ */
+#define metamacro_if_eq_recursive(A, B) \
+ metamacro_concat(metamacro_if_eq_recursive, A)(B)
+
+/**
+ * Returns 1 if N is an even number, or 0 otherwise. N must be between zero and
+ * twenty, inclusive.
+ *
+ * For the purposes of this test, zero is considered even.
+ */
+#define metamacro_is_even(N) \
+ metamacro_at(N, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1)
+
+/**
+ * Returns the logical NOT of B, which must be the number zero or one.
+ */
+#define metamacro_not(B) \
+ metamacro_at(B, 1, 0)
+
+// IMPLEMENTATION DETAILS FOLLOW!
+// Do not write code that depends on anything below this line.
+#define metamacro_stringify_(VALUE) # VALUE
+#define metamacro_concat_(A, B) A ## B
+#define metamacro_foreach_iter(INDEX, MACRO, ARG) MACRO(INDEX, ARG)
+#define metamacro_head_(FIRST, ...) FIRST
+#define metamacro_tail_(FIRST, ...) __VA_ARGS__
+#define metamacro_consume_(...)
+#define metamacro_expand_(...) __VA_ARGS__
+
+// implemented from scratch so that metamacro_concat() doesn't end up nesting
+#define metamacro_foreach_concat_iter(INDEX, BASE, ARG) metamacro_foreach_concat_iter_(BASE, ARG)
+#define metamacro_foreach_concat_iter_(BASE, ARG) BASE ## ARG
+
+// metamacro_at expansions
+#define metamacro_at0(...) metamacro_head(__VA_ARGS__)
+#define metamacro_at1(_0, ...) metamacro_head(__VA_ARGS__)
+#define metamacro_at2(_0, _1, ...) metamacro_head(__VA_ARGS__)
+#define metamacro_at3(_0, _1, _2, ...) metamacro_head(__VA_ARGS__)
+#define metamacro_at4(_0, _1, _2, _3, ...) metamacro_head(__VA_ARGS__)
+#define metamacro_at5(_0, _1, _2, _3, _4, ...) metamacro_head(__VA_ARGS__)
+#define metamacro_at6(_0, _1, _2, _3, _4, _5, ...) metamacro_head(__VA_ARGS__)
+#define metamacro_at7(_0, _1, _2, _3, _4, _5, _6, ...) metamacro_head(__VA_ARGS__)
+#define metamacro_at8(_0, _1, _2, _3, _4, _5, _6, _7, ...) metamacro_head(__VA_ARGS__)
+#define metamacro_at9(_0, _1, _2, _3, _4, _5, _6, _7, _8, ...) metamacro_head(__VA_ARGS__)
+#define metamacro_at10(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, ...) metamacro_head(__VA_ARGS__)
+#define metamacro_at11(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, ...) metamacro_head(__VA_ARGS__)
+#define metamacro_at12(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, ...) metamacro_head(__VA_ARGS__)
+#define metamacro_at13(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, ...) metamacro_head(__VA_ARGS__)
+#define metamacro_at14(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, ...) metamacro_head(__VA_ARGS__)
+#define metamacro_at15(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, ...) metamacro_head(__VA_ARGS__)
+#define metamacro_at16(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, ...) metamacro_head(__VA_ARGS__)
+#define metamacro_at17(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, ...) metamacro_head(__VA_ARGS__)
+#define metamacro_at18(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, ...) metamacro_head(__VA_ARGS__)
+#define metamacro_at19(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, ...) metamacro_head(__VA_ARGS__)
+#define metamacro_at20(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, ...) metamacro_head(__VA_ARGS__)
+
+// metamacro_foreach_cxt expansions
+#define metamacro_foreach_cxt0(MACRO, SEP, CONTEXT)
+#define metamacro_foreach_cxt1(MACRO, SEP, CONTEXT, _0) MACRO(0, CONTEXT, _0)
+
+#define metamacro_foreach_cxt2(MACRO, SEP, CONTEXT, _0, _1) \
+ metamacro_foreach_cxt1(MACRO, SEP, CONTEXT, _0) \
+ SEP \
+ MACRO(1, CONTEXT, _1)
+
+#define metamacro_foreach_cxt3(MACRO, SEP, CONTEXT, _0, _1, _2) \
+ metamacro_foreach_cxt2(MACRO, SEP, CONTEXT, _0, _1) \
+ SEP \
+ MACRO(2, CONTEXT, _2)
+
+#define metamacro_foreach_cxt4(MACRO, SEP, CONTEXT, _0, _1, _2, _3) \
+ metamacro_foreach_cxt3(MACRO, SEP, CONTEXT, _0, _1, _2) \
+ SEP \
+ MACRO(3, CONTEXT, _3)
+
+#define metamacro_foreach_cxt5(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4) \
+ metamacro_foreach_cxt4(MACRO, SEP, CONTEXT, _0, _1, _2, _3) \
+ SEP \
+ MACRO(4, CONTEXT, _4)
+
+#define metamacro_foreach_cxt6(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5) \
+ metamacro_foreach_cxt5(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4) \
+ SEP \
+ MACRO(5, CONTEXT, _5)
+
+#define metamacro_foreach_cxt7(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6) \
+ metamacro_foreach_cxt6(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5) \
+ SEP \
+ MACRO(6, CONTEXT, _6)
+
+#define metamacro_foreach_cxt8(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7) \
+ metamacro_foreach_cxt7(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6) \
+ SEP \
+ MACRO(7, CONTEXT, _7)
+
+#define metamacro_foreach_cxt9(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8) \
+ metamacro_foreach_cxt8(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7) \
+ SEP \
+ MACRO(8, CONTEXT, _8)
+
+#define metamacro_foreach_cxt10(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9) \
+ metamacro_foreach_cxt9(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8) \
+ SEP \
+ MACRO(9, CONTEXT, _9)
+
+#define metamacro_foreach_cxt11(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10) \
+ metamacro_foreach_cxt10(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9) \
+ SEP \
+ MACRO(10, CONTEXT, _10)
+
+#define metamacro_foreach_cxt12(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11) \
+ metamacro_foreach_cxt11(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10) \
+ SEP \
+ MACRO(11, CONTEXT, _11)
+
+#define metamacro_foreach_cxt13(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12) \
+ metamacro_foreach_cxt12(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11) \
+ SEP \
+ MACRO(12, CONTEXT, _12)
+
+#define metamacro_foreach_cxt14(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13) \
+ metamacro_foreach_cxt13(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12) \
+ SEP \
+ MACRO(13, CONTEXT, _13)
+
+#define metamacro_foreach_cxt15(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14) \
+ metamacro_foreach_cxt14(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13) \
+ SEP \
+ MACRO(14, CONTEXT, _14)
+
+#define metamacro_foreach_cxt16(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15) \
+ metamacro_foreach_cxt15(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14) \
+ SEP \
+ MACRO(15, CONTEXT, _15)
+
+#define metamacro_foreach_cxt17(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16) \
+ metamacro_foreach_cxt16(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15) \
+ SEP \
+ MACRO(16, CONTEXT, _16)
+
+#define metamacro_foreach_cxt18(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17) \
+ metamacro_foreach_cxt17(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16) \
+ SEP \
+ MACRO(17, CONTEXT, _17)
+
+#define metamacro_foreach_cxt19(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18) \
+ metamacro_foreach_cxt18(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17) \
+ SEP \
+ MACRO(18, CONTEXT, _18)
+
+#define metamacro_foreach_cxt20(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19) \
+ metamacro_foreach_cxt19(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18) \
+ SEP \
+ MACRO(19, CONTEXT, _19)
+
+// metamacro_foreach_cxt_recursive expansions
+#define metamacro_foreach_cxt_recursive0(MACRO, SEP, CONTEXT)
+#define metamacro_foreach_cxt_recursive1(MACRO, SEP, CONTEXT, _0) MACRO(0, CONTEXT, _0)
+
+#define metamacro_foreach_cxt_recursive2(MACRO, SEP, CONTEXT, _0, _1) \
+ metamacro_foreach_cxt_recursive1(MACRO, SEP, CONTEXT, _0) \
+ SEP \
+ MACRO(1, CONTEXT, _1)
+
+#define metamacro_foreach_cxt_recursive3(MACRO, SEP, CONTEXT, _0, _1, _2) \
+ metamacro_foreach_cxt_recursive2(MACRO, SEP, CONTEXT, _0, _1) \
+ SEP \
+ MACRO(2, CONTEXT, _2)
+
+#define metamacro_foreach_cxt_recursive4(MACRO, SEP, CONTEXT, _0, _1, _2, _3) \
+ metamacro_foreach_cxt_recursive3(MACRO, SEP, CONTEXT, _0, _1, _2) \
+ SEP \
+ MACRO(3, CONTEXT, _3)
+
+#define metamacro_foreach_cxt_recursive5(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4) \
+ metamacro_foreach_cxt_recursive4(MACRO, SEP, CONTEXT, _0, _1, _2, _3) \
+ SEP \
+ MACRO(4, CONTEXT, _4)
+
+#define metamacro_foreach_cxt_recursive6(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5) \
+ metamacro_foreach_cxt_recursive5(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4) \
+ SEP \
+ MACRO(5, CONTEXT, _5)
+
+#define metamacro_foreach_cxt_recursive7(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6) \
+ metamacro_foreach_cxt_recursive6(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5) \
+ SEP \
+ MACRO(6, CONTEXT, _6)
+
+#define metamacro_foreach_cxt_recursive8(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7) \
+ metamacro_foreach_cxt_recursive7(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6) \
+ SEP \
+ MACRO(7, CONTEXT, _7)
+
+#define metamacro_foreach_cxt_recursive9(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8) \
+ metamacro_foreach_cxt_recursive8(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7) \
+ SEP \
+ MACRO(8, CONTEXT, _8)
+
+#define metamacro_foreach_cxt_recursive10(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9) \
+ metamacro_foreach_cxt_recursive9(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8) \
+ SEP \
+ MACRO(9, CONTEXT, _9)
+
+#define metamacro_foreach_cxt_recursive11(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10) \
+ metamacro_foreach_cxt_recursive10(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9) \
+ SEP \
+ MACRO(10, CONTEXT, _10)
+
+#define metamacro_foreach_cxt_recursive12(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11) \
+ metamacro_foreach_cxt_recursive11(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10) \
+ SEP \
+ MACRO(11, CONTEXT, _11)
+
+#define metamacro_foreach_cxt_recursive13(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12) \
+ metamacro_foreach_cxt_recursive12(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11) \
+ SEP \
+ MACRO(12, CONTEXT, _12)
+
+#define metamacro_foreach_cxt_recursive14(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13) \
+ metamacro_foreach_cxt_recursive13(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12) \
+ SEP \
+ MACRO(13, CONTEXT, _13)
+
+#define metamacro_foreach_cxt_recursive15(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14) \
+ metamacro_foreach_cxt_recursive14(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13) \
+ SEP \
+ MACRO(14, CONTEXT, _14)
+
+#define metamacro_foreach_cxt_recursive16(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15) \
+ metamacro_foreach_cxt_recursive15(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14) \
+ SEP \
+ MACRO(15, CONTEXT, _15)
+
+#define metamacro_foreach_cxt_recursive17(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16) \
+ metamacro_foreach_cxt_recursive16(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15) \
+ SEP \
+ MACRO(16, CONTEXT, _16)
+
+#define metamacro_foreach_cxt_recursive18(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17) \
+ metamacro_foreach_cxt_recursive17(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16) \
+ SEP \
+ MACRO(17, CONTEXT, _17)
+
+#define metamacro_foreach_cxt_recursive19(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18) \
+ metamacro_foreach_cxt_recursive18(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17) \
+ SEP \
+ MACRO(18, CONTEXT, _18)
+
+#define metamacro_foreach_cxt_recursive20(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19) \
+ metamacro_foreach_cxt_recursive19(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18) \
+ SEP \
+ MACRO(19, CONTEXT, _19)
+
+// metamacro_for_cxt expansions
+#define metamacro_for_cxt0(MACRO, SEP, CONTEXT)
+#define metamacro_for_cxt1(MACRO, SEP, CONTEXT) MACRO(0, CONTEXT)
+
+#define metamacro_for_cxt2(MACRO, SEP, CONTEXT) \
+ metamacro_for_cxt1(MACRO, SEP, CONTEXT) \
+ SEP \
+ MACRO(1, CONTEXT)
+
+#define metamacro_for_cxt3(MACRO, SEP, CONTEXT) \
+ metamacro_for_cxt2(MACRO, SEP, CONTEXT) \
+ SEP \
+ MACRO(2, CONTEXT)
+
+#define metamacro_for_cxt4(MACRO, SEP, CONTEXT) \
+ metamacro_for_cxt3(MACRO, SEP, CONTEXT) \
+ SEP \
+ MACRO(3, CONTEXT)
+
+#define metamacro_for_cxt5(MACRO, SEP, CONTEXT) \
+ metamacro_for_cxt4(MACRO, SEP, CONTEXT) \
+ SEP \
+ MACRO(4, CONTEXT)
+
+#define metamacro_for_cxt6(MACRO, SEP, CONTEXT) \
+ metamacro_for_cxt5(MACRO, SEP, CONTEXT) \
+ SEP \
+ MACRO(5, CONTEXT)
+
+#define metamacro_for_cxt7(MACRO, SEP, CONTEXT) \
+ metamacro_for_cxt6(MACRO, SEP, CONTEXT) \
+ SEP \
+ MACRO(6, CONTEXT)
+
+#define metamacro_for_cxt8(MACRO, SEP, CONTEXT) \
+ metamacro_for_cxt7(MACRO, SEP, CONTEXT) \
+ SEP \
+ MACRO(7, CONTEXT)
+
+#define metamacro_for_cxt9(MACRO, SEP, CONTEXT) \
+ metamacro_for_cxt8(MACRO, SEP, CONTEXT) \
+ SEP \
+ MACRO(8, CONTEXT)
+
+#define metamacro_for_cxt10(MACRO, SEP, CONTEXT) \
+ metamacro_for_cxt9(MACRO, SEP, CONTEXT) \
+ SEP \
+ MACRO(9, CONTEXT)
+
+#define metamacro_for_cxt11(MACRO, SEP, CONTEXT) \
+ metamacro_for_cxt10(MACRO, SEP, CONTEXT) \
+ SEP \
+ MACRO(10, CONTEXT)
+
+#define metamacro_for_cxt12(MACRO, SEP, CONTEXT) \
+ metamacro_for_cxt11(MACRO, SEP, CONTEXT) \
+ SEP \
+ MACRO(11, CONTEXT)
+
+#define metamacro_for_cxt13(MACRO, SEP, CONTEXT) \
+ metamacro_for_cxt12(MACRO, SEP, CONTEXT) \
+ SEP \
+ MACRO(12, CONTEXT)
+
+#define metamacro_for_cxt14(MACRO, SEP, CONTEXT) \
+ metamacro_for_cxt13(MACRO, SEP, CONTEXT) \
+ SEP \
+ MACRO(13, CONTEXT)
+
+#define metamacro_for_cxt15(MACRO, SEP, CONTEXT) \
+ metamacro_for_cxt14(MACRO, SEP, CONTEXT) \
+ SEP \
+ MACRO(14, CONTEXT)
+
+#define metamacro_for_cxt16(MACRO, SEP, CONTEXT) \
+ metamacro_for_cxt15(MACRO, SEP, CONTEXT) \
+ SEP \
+ MACRO(15, CONTEXT)
+
+#define metamacro_for_cxt17(MACRO, SEP, CONTEXT) \
+ metamacro_for_cxt16(MACRO, SEP, CONTEXT) \
+ SEP \
+ MACRO(16, CONTEXT)
+
+#define metamacro_for_cxt18(MACRO, SEP, CONTEXT) \
+ metamacro_for_cxt17(MACRO, SEP, CONTEXT) \
+ SEP \
+ MACRO(17, CONTEXT)
+
+#define metamacro_for_cxt19(MACRO, SEP, CONTEXT) \
+ metamacro_for_cxt18(MACRO, SEP, CONTEXT) \
+ SEP \
+ MACRO(18, CONTEXT)
+
+#define metamacro_for_cxt20(MACRO, SEP, CONTEXT) \
+ metamacro_for_cxt19(MACRO, SEP, CONTEXT) \
+ SEP \
+ MACRO(19, CONTEXT)
+
+// metamacro_if_eq expansions
+#define metamacro_if_eq0(VALUE) \
+ metamacro_concat(metamacro_if_eq0_, VALUE)
+
+#define metamacro_if_eq0_0(...) __VA_ARGS__ metamacro_consume_
+#define metamacro_if_eq0_1(...) metamacro_expand_
+#define metamacro_if_eq0_2(...) metamacro_expand_
+#define metamacro_if_eq0_3(...) metamacro_expand_
+#define metamacro_if_eq0_4(...) metamacro_expand_
+#define metamacro_if_eq0_5(...) metamacro_expand_
+#define metamacro_if_eq0_6(...) metamacro_expand_
+#define metamacro_if_eq0_7(...) metamacro_expand_
+#define metamacro_if_eq0_8(...) metamacro_expand_
+#define metamacro_if_eq0_9(...) metamacro_expand_
+#define metamacro_if_eq0_10(...) metamacro_expand_
+#define metamacro_if_eq0_11(...) metamacro_expand_
+#define metamacro_if_eq0_12(...) metamacro_expand_
+#define metamacro_if_eq0_13(...) metamacro_expand_
+#define metamacro_if_eq0_14(...) metamacro_expand_
+#define metamacro_if_eq0_15(...) metamacro_expand_
+#define metamacro_if_eq0_16(...) metamacro_expand_
+#define metamacro_if_eq0_17(...) metamacro_expand_
+#define metamacro_if_eq0_18(...) metamacro_expand_
+#define metamacro_if_eq0_19(...) metamacro_expand_
+#define metamacro_if_eq0_20(...) metamacro_expand_
+
+#define metamacro_if_eq1(VALUE) metamacro_if_eq0(metamacro_dec(VALUE))
+#define metamacro_if_eq2(VALUE) metamacro_if_eq1(metamacro_dec(VALUE))
+#define metamacro_if_eq3(VALUE) metamacro_if_eq2(metamacro_dec(VALUE))
+#define metamacro_if_eq4(VALUE) metamacro_if_eq3(metamacro_dec(VALUE))
+#define metamacro_if_eq5(VALUE) metamacro_if_eq4(metamacro_dec(VALUE))
+#define metamacro_if_eq6(VALUE) metamacro_if_eq5(metamacro_dec(VALUE))
+#define metamacro_if_eq7(VALUE) metamacro_if_eq6(metamacro_dec(VALUE))
+#define metamacro_if_eq8(VALUE) metamacro_if_eq7(metamacro_dec(VALUE))
+#define metamacro_if_eq9(VALUE) metamacro_if_eq8(metamacro_dec(VALUE))
+#define metamacro_if_eq10(VALUE) metamacro_if_eq9(metamacro_dec(VALUE))
+#define metamacro_if_eq11(VALUE) metamacro_if_eq10(metamacro_dec(VALUE))
+#define metamacro_if_eq12(VALUE) metamacro_if_eq11(metamacro_dec(VALUE))
+#define metamacro_if_eq13(VALUE) metamacro_if_eq12(metamacro_dec(VALUE))
+#define metamacro_if_eq14(VALUE) metamacro_if_eq13(metamacro_dec(VALUE))
+#define metamacro_if_eq15(VALUE) metamacro_if_eq14(metamacro_dec(VALUE))
+#define metamacro_if_eq16(VALUE) metamacro_if_eq15(metamacro_dec(VALUE))
+#define metamacro_if_eq17(VALUE) metamacro_if_eq16(metamacro_dec(VALUE))
+#define metamacro_if_eq18(VALUE) metamacro_if_eq17(metamacro_dec(VALUE))
+#define metamacro_if_eq19(VALUE) metamacro_if_eq18(metamacro_dec(VALUE))
+#define metamacro_if_eq20(VALUE) metamacro_if_eq19(metamacro_dec(VALUE))
+
+// metamacro_if_eq_recursive expansions
+#define metamacro_if_eq_recursive0(VALUE) \
+ metamacro_concat(metamacro_if_eq_recursive0_, VALUE)
+
+#define metamacro_if_eq_recursive0_0(...) __VA_ARGS__ metamacro_consume_
+#define metamacro_if_eq_recursive0_1(...) metamacro_expand_
+#define metamacro_if_eq_recursive0_2(...) metamacro_expand_
+#define metamacro_if_eq_recursive0_3(...) metamacro_expand_
+#define metamacro_if_eq_recursive0_4(...) metamacro_expand_
+#define metamacro_if_eq_recursive0_5(...) metamacro_expand_
+#define metamacro_if_eq_recursive0_6(...) metamacro_expand_
+#define metamacro_if_eq_recursive0_7(...) metamacro_expand_
+#define metamacro_if_eq_recursive0_8(...) metamacro_expand_
+#define metamacro_if_eq_recursive0_9(...) metamacro_expand_
+#define metamacro_if_eq_recursive0_10(...) metamacro_expand_
+#define metamacro_if_eq_recursive0_11(...) metamacro_expand_
+#define metamacro_if_eq_recursive0_12(...) metamacro_expand_
+#define metamacro_if_eq_recursive0_13(...) metamacro_expand_
+#define metamacro_if_eq_recursive0_14(...) metamacro_expand_
+#define metamacro_if_eq_recursive0_15(...) metamacro_expand_
+#define metamacro_if_eq_recursive0_16(...) metamacro_expand_
+#define metamacro_if_eq_recursive0_17(...) metamacro_expand_
+#define metamacro_if_eq_recursive0_18(...) metamacro_expand_
+#define metamacro_if_eq_recursive0_19(...) metamacro_expand_
+#define metamacro_if_eq_recursive0_20(...) metamacro_expand_
+
+#define metamacro_if_eq_recursive1(VALUE) metamacro_if_eq_recursive0(metamacro_dec(VALUE))
+#define metamacro_if_eq_recursive2(VALUE) metamacro_if_eq_recursive1(metamacro_dec(VALUE))
+#define metamacro_if_eq_recursive3(VALUE) metamacro_if_eq_recursive2(metamacro_dec(VALUE))
+#define metamacro_if_eq_recursive4(VALUE) metamacro_if_eq_recursive3(metamacro_dec(VALUE))
+#define metamacro_if_eq_recursive5(VALUE) metamacro_if_eq_recursive4(metamacro_dec(VALUE))
+#define metamacro_if_eq_recursive6(VALUE) metamacro_if_eq_recursive5(metamacro_dec(VALUE))
+#define metamacro_if_eq_recursive7(VALUE) metamacro_if_eq_recursive6(metamacro_dec(VALUE))
+#define metamacro_if_eq_recursive8(VALUE) metamacro_if_eq_recursive7(metamacro_dec(VALUE))
+#define metamacro_if_eq_recursive9(VALUE) metamacro_if_eq_recursive8(metamacro_dec(VALUE))
+#define metamacro_if_eq_recursive10(VALUE) metamacro_if_eq_recursive9(metamacro_dec(VALUE))
+#define metamacro_if_eq_recursive11(VALUE) metamacro_if_eq_recursive10(metamacro_dec(VALUE))
+#define metamacro_if_eq_recursive12(VALUE) metamacro_if_eq_recursive11(metamacro_dec(VALUE))
+#define metamacro_if_eq_recursive13(VALUE) metamacro_if_eq_recursive12(metamacro_dec(VALUE))
+#define metamacro_if_eq_recursive14(VALUE) metamacro_if_eq_recursive13(metamacro_dec(VALUE))
+#define metamacro_if_eq_recursive15(VALUE) metamacro_if_eq_recursive14(metamacro_dec(VALUE))
+#define metamacro_if_eq_recursive16(VALUE) metamacro_if_eq_recursive15(metamacro_dec(VALUE))
+#define metamacro_if_eq_recursive17(VALUE) metamacro_if_eq_recursive16(metamacro_dec(VALUE))
+#define metamacro_if_eq_recursive18(VALUE) metamacro_if_eq_recursive17(metamacro_dec(VALUE))
+#define metamacro_if_eq_recursive19(VALUE) metamacro_if_eq_recursive18(metamacro_dec(VALUE))
+#define metamacro_if_eq_recursive20(VALUE) metamacro_if_eq_recursive19(metamacro_dec(VALUE))
+
+// metamacro_take expansions
+#define metamacro_take0(...)
+#define metamacro_take1(...) metamacro_head(__VA_ARGS__)
+#define metamacro_take2(...) metamacro_head(__VA_ARGS__), metamacro_take1(metamacro_tail(__VA_ARGS__))
+#define metamacro_take3(...) metamacro_head(__VA_ARGS__), metamacro_take2(metamacro_tail(__VA_ARGS__))
+#define metamacro_take4(...) metamacro_head(__VA_ARGS__), metamacro_take3(metamacro_tail(__VA_ARGS__))
+#define metamacro_take5(...) metamacro_head(__VA_ARGS__), metamacro_take4(metamacro_tail(__VA_ARGS__))
+#define metamacro_take6(...) metamacro_head(__VA_ARGS__), metamacro_take5(metamacro_tail(__VA_ARGS__))
+#define metamacro_take7(...) metamacro_head(__VA_ARGS__), metamacro_take6(metamacro_tail(__VA_ARGS__))
+#define metamacro_take8(...) metamacro_head(__VA_ARGS__), metamacro_take7(metamacro_tail(__VA_ARGS__))
+#define metamacro_take9(...) metamacro_head(__VA_ARGS__), metamacro_take8(metamacro_tail(__VA_ARGS__))
+#define metamacro_take10(...) metamacro_head(__VA_ARGS__), metamacro_take9(metamacro_tail(__VA_ARGS__))
+#define metamacro_take11(...) metamacro_head(__VA_ARGS__), metamacro_take10(metamacro_tail(__VA_ARGS__))
+#define metamacro_take12(...) metamacro_head(__VA_ARGS__), metamacro_take11(metamacro_tail(__VA_ARGS__))
+#define metamacro_take13(...) metamacro_head(__VA_ARGS__), metamacro_take12(metamacro_tail(__VA_ARGS__))
+#define metamacro_take14(...) metamacro_head(__VA_ARGS__), metamacro_take13(metamacro_tail(__VA_ARGS__))
+#define metamacro_take15(...) metamacro_head(__VA_ARGS__), metamacro_take14(metamacro_tail(__VA_ARGS__))
+#define metamacro_take16(...) metamacro_head(__VA_ARGS__), metamacro_take15(metamacro_tail(__VA_ARGS__))
+#define metamacro_take17(...) metamacro_head(__VA_ARGS__), metamacro_take16(metamacro_tail(__VA_ARGS__))
+#define metamacro_take18(...) metamacro_head(__VA_ARGS__), metamacro_take17(metamacro_tail(__VA_ARGS__))
+#define metamacro_take19(...) metamacro_head(__VA_ARGS__), metamacro_take18(metamacro_tail(__VA_ARGS__))
+#define metamacro_take20(...) metamacro_head(__VA_ARGS__), metamacro_take19(metamacro_tail(__VA_ARGS__))
+
+// metamacro_drop expansions
+#define metamacro_drop0(...) __VA_ARGS__
+#define metamacro_drop1(...) metamacro_tail(__VA_ARGS__)
+#define metamacro_drop2(...) metamacro_drop1(metamacro_tail(__VA_ARGS__))
+#define metamacro_drop3(...) metamacro_drop2(metamacro_tail(__VA_ARGS__))
+#define metamacro_drop4(...) metamacro_drop3(metamacro_tail(__VA_ARGS__))
+#define metamacro_drop5(...) metamacro_drop4(metamacro_tail(__VA_ARGS__))
+#define metamacro_drop6(...) metamacro_drop5(metamacro_tail(__VA_ARGS__))
+#define metamacro_drop7(...) metamacro_drop6(metamacro_tail(__VA_ARGS__))
+#define metamacro_drop8(...) metamacro_drop7(metamacro_tail(__VA_ARGS__))
+#define metamacro_drop9(...) metamacro_drop8(metamacro_tail(__VA_ARGS__))
+#define metamacro_drop10(...) metamacro_drop9(metamacro_tail(__VA_ARGS__))
+#define metamacro_drop11(...) metamacro_drop10(metamacro_tail(__VA_ARGS__))
+#define metamacro_drop12(...) metamacro_drop11(metamacro_tail(__VA_ARGS__))
+#define metamacro_drop13(...) metamacro_drop12(metamacro_tail(__VA_ARGS__))
+#define metamacro_drop14(...) metamacro_drop13(metamacro_tail(__VA_ARGS__))
+#define metamacro_drop15(...) metamacro_drop14(metamacro_tail(__VA_ARGS__))
+#define metamacro_drop16(...) metamacro_drop15(metamacro_tail(__VA_ARGS__))
+#define metamacro_drop17(...) metamacro_drop16(metamacro_tail(__VA_ARGS__))
+#define metamacro_drop18(...) metamacro_drop17(metamacro_tail(__VA_ARGS__))
+#define metamacro_drop19(...) metamacro_drop18(metamacro_tail(__VA_ARGS__))
+#define metamacro_drop20(...) metamacro_drop19(metamacro_tail(__VA_ARGS__))
+
+#endif
diff --git a/Apps/Wikipedia/WMF Framework/Third Party/Mantle/include/MTLJSONAdapter.h b/Apps/Wikipedia/WMF Framework/Third Party/Mantle/include/MTLJSONAdapter.h
new file mode 100644
index 0000000..42a7e82
--- /dev/null
+++ b/Apps/Wikipedia/WMF Framework/Third Party/Mantle/include/MTLJSONAdapter.h
@@ -0,0 +1,287 @@
+//
+// MTLJSONAdapter.h
+// Mantle
+//
+// Created by Justin Spahr-Summers on 2013-02-12.
+// Copyright (c) 2013 GitHub. All rights reserved.
+//
+
+#import
+
+@protocol MTLModel;
+@protocol MTLTransformerErrorHandling;
+
+/// A MTLModel object that supports being parsed from and serialized to JSON.
+@protocol MTLJSONSerializing