Javier Cicchelli 9bcdaa697b [Setup] Basic project structure (#1)
This PR contains all the work related to setting up this project as required to implement the [Assignment](https://repo.rock-n-code.com/rock-n-code/deep-linking-assignment/wiki/Assignment) on top, as intended.

To summarise this work:
- [x] created a new **Xcode** project;
- [x] cloned the `Wikipedia` app and inserted it into the **Xcode** project;
- [x] created the `Locations` app and also, its `Libraries` package;
- [x] created the `Shared` package to share dependencies between the apps;
- [x] added a `Makefile` file and implemented some **environment** and **help** commands.

Co-authored-by: Javier Cicchelli <javier@rock-n-code.com>
Reviewed-on: rock-n-code/deep-linking-assignment#1
2023-04-08 18:37:13 +00:00

259 lines
10 KiB
Swift

@objc(WMFRouter)
public class Router: NSObject {
public enum Destination: Equatable {
case inAppLink(_: URL)
case externalLink(_: URL)
case article(_: URL)
case articleHistory(_: URL, articleTitle: String)
case articleDiffCompare(_: URL, fromRevID: Int?, toRevID: Int?)
case articleDiffSingle(_: URL, fromRevID: Int?, toRevID: Int?)
case talk(_: URL)
case userTalk(_: URL)
case search(_: URL, term: String?)
case audio(_: URL)
case onThisDay(_: Int?)
case readingListsImport(encodedPayload: String)
case login
}
unowned let configuration: Configuration
required init(configuration: Configuration) {
self.configuration = configuration
}
// MARK: Public
/// Gets the appropriate in-app destination for a given URL
public func destination(for url: URL, loggedInUsername: String?) -> Destination {
guard let siteURL = url.wmf_site,
let project = WikimediaProject(siteURL: siteURL) else {
guard url.isWikimediaHostedAudioFileLink else {
return webViewDestinationForHostURL(url)
}
return .audio(url.byMakingAudioFileCompatibilityAdjustments)
}
return destinationForHostURL(url, project: project, loggedInUsername: loggedInUsername)
}
public func doesOpenInBrowser(for url: URL, loggedInUsername: String?) -> Bool {
return [.externalLink(url), .inAppLink(url)].contains(destination(for: url, loggedInUsername: loggedInUsername))
}
// MARK: Internal and Private
private let mobilediffRegexCompare = try! NSRegularExpression(pattern: "^mobilediff/([0-9]+)\\.\\.\\.([0-9]+)", options: .caseInsensitive)
private let mobilediffRegexSingle = try! NSRegularExpression(pattern: "^mobilediff/([0-9]+)", options: .caseInsensitive)
private let historyRegex = try! NSRegularExpression(pattern: "^history/(.*)", options: .caseInsensitive)
internal func destinationForWikiResourceURL(_ url: URL, project: WikimediaProject, loggedInUsername: String?) -> Destination? {
guard let path = url.wikiResourcePath else {
return nil
}
let language = project.languageCode ?? "en"
let namespaceAndTitle = path.namespaceAndTitleOfWikiResourcePath(with: language)
let namespace = namespaceAndTitle.0
let title = namespaceAndTitle.1
switch namespace {
case .talk:
if FeatureFlags.needsNewTalkPage && project.supportsNativeUserTalkPages {
return .talk(url)
} else {
return nil
}
case .userTalk:
return project.supportsNativeUserTalkPages ? .userTalk(url) : nil
case .special:
// TODO: Fix to work across languages, not just EN. Fetch special page aliases per site and add to a set of local json files.
// https://en.wikipedia.org/w/api.php?action=query&format=json&meta=siteinfo&formatversion=2&siprop=specialpagealiases
if language.uppercased() == "EN" || language.uppercased() == "TEST",
title == "MyTalk",
let username = loggedInUsername,
let newURL = url.wmf_URL(withTitle: "User_talk:\(username)") {
return .userTalk(newURL)
}
if language.uppercased() == "EN" || language.uppercased() == "TEST",
title == "MyContributions",
let username = loggedInUsername,
let newURL = url.wmf_URL(withPath: "/wiki/Special:Contributions/\(username)", isMobile: true) {
return .inAppLink(newURL)
}
if language.uppercased() == "EN" || language.uppercased() == "TEST",
title == "UserLogin" {
return .login
}
if title == "ReadingLists",
let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
let firstQueryItem = components.queryItems?.first,
firstQueryItem.name == "limport",
let encodedPayload = firstQueryItem.value {
return .readingListsImport(encodedPayload: encodedPayload)
}
guard project.supportsNativeDiffPages else {
return nil
}
if let compareDiffMatch = mobilediffRegexCompare.firstMatch(in: title),
let fromRevID = Int(mobilediffRegexCompare.replacementString(for: compareDiffMatch, in: title, offset: 0, template: "$1")),
let toRevID = Int(mobilediffRegexCompare.replacementString(for: compareDiffMatch, in: title, offset: 0, template: "$2")) {
return .articleDiffCompare(url, fromRevID: fromRevID, toRevID: toRevID)
}
if let singleDiffMatch = mobilediffRegexSingle.firstReplacementString(in: title),
let toRevID = Int(singleDiffMatch) {
return .articleDiffSingle(url, fromRevID: nil, toRevID: toRevID)
}
if let articleTitle = historyRegex.firstReplacementString(in: title)?.normalizedPageTitle {
return .articleHistory(url, articleTitle: articleTitle)
}
return nil
case .main:
guard project.mainNamespaceGoesToNativeArticleView else {
return nil
}
return WikipediaURLTranslations.isMainpageTitle(title, in: language) ? nil : Destination.article(url)
case .wikipedia:
guard project.considersWResourcePathsForRouting else {
return nil
}
let onThisDayURLSnippet = "On_this_day"
if title.uppercased().contains(onThisDayURLSnippet.uppercased()) {
// URL in form of https://en.wikipedia.org/wiki/Wikipedia:On_this_day/Today?3. Take bit past question mark.
if let selected = url.query {
return .onThisDay(Int(selected))
} else {
return .onThisDay(nil)
}
} else {
fallthrough
}
default:
return nil
}
}
internal func destinationForWResourceURL(_ url: URL, project: WikimediaProject) -> Destination? {
guard project.considersWResourcePathsForRouting,
let path = url.wResourcePath else {
return nil
}
guard var components = URLComponents(string: path) else {
return nil
}
components.query = url.query
guard components.path.lowercased() == "index.php" else {
return nil
}
guard let queryItems = components.queryItems else {
return nil
}
var params: [String: String] = [:]
params.reserveCapacity(queryItems.count)
for item in queryItems {
params[item.name] = item.value
}
if let search = params["search"] {
return .search(url, term: search)
}
let maybeTitle = params["title"]
let maybeDiff = params["diff"]
let maybeOldID = params["oldid"]
let maybeType = params["type"]
let maybeAction = params["action"]
let maybeDir = params["dir"]
let maybeLimit = params["limit"]
guard let title = maybeTitle else {
return nil
}
let language = project.languageCode ?? "en"
if language.uppercased() == "EN" || language.uppercased() == "TEST",
title == "Special:UserLogin" {
return .login
}
if maybeLimit != nil,
maybeDir != nil,
let action = maybeAction,
action == "history" {
// TODO: push history 'slice'
return .articleHistory(url, articleTitle: title)
} else if let action = maybeAction,
action == "history" {
return .articleHistory(url, articleTitle: title)
} else if let type = maybeType,
type == "revision",
let diffString = maybeDiff,
let oldIDString = maybeOldID,
let toRevID = Int(diffString),
let fromRevID = Int(oldIDString) {
return .articleDiffCompare(url, fromRevID: fromRevID, toRevID: toRevID)
} else if let diff = maybeDiff,
diff == "prev",
let oldIDString = maybeOldID,
let toRevID = Int(oldIDString) {
return .articleDiffCompare(url, fromRevID: nil, toRevID: toRevID)
} else if let diff = maybeDiff,
diff == "next",
let oldIDString = maybeOldID,
let fromRevID = Int(oldIDString) {
return .articleDiffCompare(url, fromRevID: fromRevID, toRevID: nil)
} else if let oldIDString = maybeOldID,
let toRevID = Int(oldIDString) {
return .articleDiffSingle(url, fromRevID: nil, toRevID: toRevID)
}
return nil
}
internal func destinationForHostURL(_ url: URL, project: WikimediaProject, loggedInUsername: String?) -> Destination {
let canonicalURL = url.canonical
if let wikiResourcePathInfo = destinationForWikiResourceURL(canonicalURL, project: project, loggedInUsername: loggedInUsername) {
return wikiResourcePathInfo
}
if let wResourcePathInfo = destinationForWResourceURL(canonicalURL, project: project) {
return wResourcePathInfo
}
return webViewDestinationForHostURL(url)
}
internal func webViewDestinationForHostURL(_ url: URL) -> Destination {
let canonicalURL = url.canonical
if configuration.hostCanRouteToInAppWebView(url.host) {
return .inAppLink(canonicalURL)
} else {
return .externalLink(url)
}
}
}