2017-05-27 10:43:27 -07:00
|
|
|
|
//
|
|
|
|
|
// ArticleRenderer.swift
|
2018-08-28 22:18:24 -07:00
|
|
|
|
// NetNewsWire
|
2017-05-27 10:43:27 -07:00
|
|
|
|
//
|
|
|
|
|
// Created by Brent Simmons on 9/8/15.
|
|
|
|
|
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
import Foundation
|
2020-03-12 18:07:40 -05:00
|
|
|
|
#if os(iOS)
|
|
|
|
|
import UIKit
|
|
|
|
|
#endif
|
2018-07-23 18:29:08 -07:00
|
|
|
|
import Articles
|
2018-07-28 12:16:14 -07:00
|
|
|
|
import Account
|
2024-03-20 20:49:15 -07:00
|
|
|
|
import Core
|
2024-04-07 15:05:38 -07:00
|
|
|
|
import ArticleExtractor
|
2017-05-27 10:43:27 -07:00
|
|
|
|
|
2024-03-19 23:05:30 -07:00
|
|
|
|
@MainActor struct ArticleRenderer {
|
2018-11-26 21:38:14 -08:00
|
|
|
|
|
2020-01-30 06:10:45 -06:00
|
|
|
|
typealias Rendering = (style: String, html: String, title: String, baseURL: String)
|
2020-03-17 17:18:24 -05:00
|
|
|
|
|
|
|
|
|
struct Page {
|
|
|
|
|
let url: URL
|
|
|
|
|
let baseURL: URL
|
|
|
|
|
let html: String
|
|
|
|
|
|
|
|
|
|
init(name: String) {
|
|
|
|
|
url = Bundle.main.url(forResource: name, withExtension: "html")!
|
|
|
|
|
baseURL = url.deletingLastPathComponent()
|
|
|
|
|
html = try! NSString(contentsOfFile: url.path, encoding: String.Encoding.utf8.rawValue) as String
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-09-21 12:36:35 -05:00
|
|
|
|
|
2024-03-19 23:05:30 -07:00
|
|
|
|
static let imageIconScheme = "nnwImageIcon"
|
|
|
|
|
|
|
|
|
|
static let blank = Page(name: "blank")
|
|
|
|
|
static let page = Page(name: "page")
|
2019-09-21 12:36:35 -05:00
|
|
|
|
|
2018-11-26 21:38:14 -08:00
|
|
|
|
private let article: Article?
|
2019-09-18 18:15:55 -05:00
|
|
|
|
private let extractedArticle: ExtractedArticle?
|
2021-09-07 16:58:06 -05:00
|
|
|
|
private let articleTheme: ArticleTheme
|
2018-11-26 21:38:14 -08:00
|
|
|
|
private let title: String
|
2019-09-18 18:15:55 -05:00
|
|
|
|
private let body: String
|
2019-04-14 12:54:17 -07:00
|
|
|
|
private let baseURL: String?
|
2021-09-17 14:10:33 -05:00
|
|
|
|
|
|
|
|
|
private static let longDateTimeFormatter: DateFormatter = {
|
|
|
|
|
let formatter = DateFormatter()
|
|
|
|
|
formatter.dateStyle = .long
|
|
|
|
|
formatter.timeStyle = .medium
|
|
|
|
|
return formatter
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
private static let mediumDateTimeFormatter: DateFormatter = {
|
|
|
|
|
let formatter = DateFormatter()
|
|
|
|
|
formatter.dateStyle = .medium
|
|
|
|
|
formatter.timeStyle = .short
|
|
|
|
|
return formatter
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
private static let shortDateTimeFormatter: DateFormatter = {
|
|
|
|
|
let formatter = DateFormatter()
|
|
|
|
|
formatter.dateStyle = .short
|
|
|
|
|
formatter.timeStyle = .short
|
|
|
|
|
return formatter
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
private static let longDateFormatter: DateFormatter = {
|
|
|
|
|
let formatter = DateFormatter()
|
|
|
|
|
formatter.dateStyle = .long
|
|
|
|
|
formatter.timeStyle = .none
|
|
|
|
|
return formatter
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
private static let mediumDateFormatter: DateFormatter = {
|
|
|
|
|
let formatter = DateFormatter()
|
|
|
|
|
formatter.dateStyle = .medium
|
|
|
|
|
formatter.timeStyle = .none
|
|
|
|
|
return formatter
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
private static let shortDateFormatter: DateFormatter = {
|
|
|
|
|
let formatter = DateFormatter()
|
|
|
|
|
formatter.dateStyle = .short
|
|
|
|
|
formatter.timeStyle = .none
|
|
|
|
|
return formatter
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
private static let longTimeFormatter: DateFormatter = {
|
|
|
|
|
let formatter = DateFormatter()
|
|
|
|
|
formatter.dateStyle = .none
|
|
|
|
|
formatter.timeStyle = .long
|
|
|
|
|
return formatter
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
private static let mediumTimeFormatter: DateFormatter = {
|
|
|
|
|
let formatter = DateFormatter()
|
|
|
|
|
formatter.dateStyle = .none
|
|
|
|
|
formatter.timeStyle = .medium
|
|
|
|
|
return formatter
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
private static let shortTimeFormatter: DateFormatter = {
|
|
|
|
|
let formatter = DateFormatter()
|
|
|
|
|
formatter.dateStyle = .none
|
|
|
|
|
formatter.timeStyle = .short
|
|
|
|
|
return formatter
|
|
|
|
|
}()
|
2018-11-26 21:38:14 -08:00
|
|
|
|
|
2021-09-07 16:58:06 -05:00
|
|
|
|
private init(article: Article?, extractedArticle: ExtractedArticle?, theme: ArticleTheme) {
|
2017-05-27 10:43:27 -07:00
|
|
|
|
self.article = article
|
2019-09-18 18:15:55 -05:00
|
|
|
|
self.extractedArticle = extractedArticle
|
2021-09-07 16:58:06 -05:00
|
|
|
|
self.articleTheme = theme
|
2020-04-30 02:36:32 -05:00
|
|
|
|
self.title = article?.sanitizedTitle() ?? ""
|
2019-09-18 18:15:55 -05:00
|
|
|
|
if let content = extractedArticle?.content {
|
|
|
|
|
self.body = content
|
2019-09-19 17:41:56 -05:00
|
|
|
|
self.baseURL = extractedArticle?.url
|
2019-09-18 18:15:55 -05:00
|
|
|
|
} else {
|
|
|
|
|
self.body = article?.body ?? ""
|
2019-09-19 17:41:56 -05:00
|
|
|
|
self.baseURL = article?.baseURL?.absoluteString
|
2019-09-18 18:15:55 -05:00
|
|
|
|
}
|
2017-05-27 10:43:27 -07:00
|
|
|
|
}
|
2019-02-10 22:06:03 -08:00
|
|
|
|
|
|
|
|
|
// MARK: - API
|
|
|
|
|
|
2021-09-07 16:58:06 -05:00
|
|
|
|
static func articleHTML(article: Article, extractedArticle: ExtractedArticle? = nil, theme: ArticleTheme) -> Rendering {
|
|
|
|
|
let renderer = ArticleRenderer(article: article, extractedArticle: extractedArticle, theme: theme)
|
2020-03-12 18:07:40 -05:00
|
|
|
|
return (renderer.articleCSS, renderer.articleHTML, renderer.title, renderer.baseURL ?? "")
|
2019-02-10 22:06:03 -08:00
|
|
|
|
}
|
|
|
|
|
|
2021-09-07 16:58:06 -05:00
|
|
|
|
static func multipleSelectionHTML(theme: ArticleTheme) -> Rendering {
|
|
|
|
|
let renderer = ArticleRenderer(article: nil, extractedArticle: nil, theme: theme)
|
2020-03-12 18:07:40 -05:00
|
|
|
|
return (renderer.articleCSS, renderer.multipleSelectionHTML, renderer.title, renderer.baseURL ?? "")
|
2019-02-10 22:06:03 -08:00
|
|
|
|
}
|
|
|
|
|
|
2021-09-07 16:58:06 -05:00
|
|
|
|
static func loadingHTML(theme: ArticleTheme) -> Rendering {
|
|
|
|
|
let renderer = ArticleRenderer(article: nil, extractedArticle: nil, theme: theme)
|
2020-03-12 18:07:40 -05:00
|
|
|
|
return (renderer.articleCSS, renderer.loadingHTML, renderer.title, renderer.baseURL ?? "")
|
2019-09-21 15:03:42 -05:00
|
|
|
|
}
|
|
|
|
|
|
2021-09-07 16:58:06 -05:00
|
|
|
|
static func noSelectionHTML(theme: ArticleTheme) -> Rendering {
|
|
|
|
|
let renderer = ArticleRenderer(article: nil, extractedArticle: nil, theme: theme)
|
2020-03-12 18:07:40 -05:00
|
|
|
|
return (renderer.articleCSS, renderer.noSelectionHTML, renderer.title, renderer.baseURL ?? "")
|
2019-02-11 22:36:31 -08:00
|
|
|
|
}
|
2019-08-31 15:03:03 -07:00
|
|
|
|
|
2021-09-07 16:58:06 -05:00
|
|
|
|
static func noContentHTML(theme: ArticleTheme) -> Rendering {
|
|
|
|
|
let renderer = ArticleRenderer(article: nil, extractedArticle: nil, theme: theme)
|
2020-03-12 18:07:40 -05:00
|
|
|
|
return (renderer.articleCSS, renderer.noContentHTML, renderer.title, renderer.baseURL ?? "")
|
2019-08-31 15:03:03 -07:00
|
|
|
|
}
|
2018-11-26 21:38:14 -08:00
|
|
|
|
}
|
2017-05-27 10:43:27 -07:00
|
|
|
|
|
2019-02-10 22:06:03 -08:00
|
|
|
|
// MARK: - Private
|
2017-05-27 10:43:27 -07:00
|
|
|
|
|
2018-11-26 21:38:14 -08:00
|
|
|
|
private extension ArticleRenderer {
|
2017-05-27 10:43:27 -07:00
|
|
|
|
|
2019-02-10 22:06:03 -08:00
|
|
|
|
private var articleHTML: String {
|
2020-03-12 18:07:40 -05:00
|
|
|
|
return try! MacroProcessor.renderedText(withTemplate: template(), substitutions: articleSubstitutions())
|
2019-02-10 22:06:03 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private var multipleSelectionHTML: String {
|
|
|
|
|
let body = "<h3 class='systemMessage'>Multiple selection</h3>"
|
2020-01-30 06:10:45 -06:00
|
|
|
|
return body
|
2019-02-10 22:06:03 -08:00
|
|
|
|
}
|
|
|
|
|
|
2019-09-21 15:03:42 -05:00
|
|
|
|
private var loadingHTML: String {
|
|
|
|
|
let body = "<h3 class='systemMessage'>Loading...</h3>"
|
2020-01-30 06:10:45 -06:00
|
|
|
|
return body
|
2019-09-21 15:03:42 -05:00
|
|
|
|
}
|
|
|
|
|
|
2019-02-10 22:06:03 -08:00
|
|
|
|
private var noSelectionHTML: String {
|
|
|
|
|
let body = "<h3 class='systemMessage'>No selection</h3>"
|
2020-01-30 06:10:45 -06:00
|
|
|
|
return body
|
2019-02-10 22:06:03 -08:00
|
|
|
|
}
|
|
|
|
|
|
2019-08-31 15:03:03 -07:00
|
|
|
|
private var noContentHTML: String {
|
2020-01-30 06:10:45 -06:00
|
|
|
|
return ""
|
2019-08-31 15:03:03 -07:00
|
|
|
|
}
|
2020-03-12 18:07:40 -05:00
|
|
|
|
|
|
|
|
|
private var articleCSS: String {
|
2020-03-20 06:41:38 -05:00
|
|
|
|
return try! MacroProcessor.renderedText(withTemplate: styleString(), substitutions: styleSubstitutions())
|
2020-03-12 18:07:40 -05:00
|
|
|
|
}
|
2019-08-31 15:03:03 -07:00
|
|
|
|
|
2018-11-26 21:38:14 -08:00
|
|
|
|
static var defaultStyleSheet: String = {
|
|
|
|
|
let path = Bundle.main.path(forResource: "styleSheet", ofType: "css")!
|
|
|
|
|
let s = try! NSString(contentsOfFile: path, encoding: String.Encoding.utf8.rawValue)
|
|
|
|
|
return "\n\(s)\n"
|
|
|
|
|
}()
|
2017-05-27 10:43:27 -07:00
|
|
|
|
|
2018-11-26 21:38:14 -08:00
|
|
|
|
static let defaultTemplate: String = {
|
|
|
|
|
let path = Bundle.main.path(forResource: "template", ofType: "html")!
|
|
|
|
|
let s = try! NSString(contentsOfFile: path, encoding: String.Encoding.utf8.rawValue)
|
|
|
|
|
return s as String
|
|
|
|
|
}()
|
2017-12-29 17:10:50 -08:00
|
|
|
|
|
2018-11-26 21:38:14 -08:00
|
|
|
|
func styleString() -> String {
|
2021-09-07 16:58:06 -05:00
|
|
|
|
return articleTheme.css ?? ArticleRenderer.defaultStyleSheet
|
2017-05-27 10:43:27 -07:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-26 21:38:14 -08:00
|
|
|
|
func template() -> String {
|
2021-09-07 16:58:06 -05:00
|
|
|
|
return articleTheme.template ?? ArticleRenderer.defaultTemplate
|
2017-12-29 11:31:47 -08:00
|
|
|
|
}
|
|
|
|
|
|
2019-09-18 18:15:55 -05:00
|
|
|
|
func articleSubstitutions() -> [String: String] {
|
2017-05-27 10:43:27 -07:00
|
|
|
|
var d = [String: String]()
|
|
|
|
|
|
2018-09-14 20:00:51 -05:00
|
|
|
|
guard let article = article else {
|
|
|
|
|
assertionFailure("Article should have been set before calling this function.")
|
|
|
|
|
return d
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-28 22:38:29 -08:00
|
|
|
|
d["title"] = title
|
2021-09-17 14:10:33 -05:00
|
|
|
|
d["preferred_link"] = article.preferredLink ?? ""
|
2020-11-20 02:17:17 -06:00
|
|
|
|
|
2021-09-30 16:46:11 +13:00
|
|
|
|
if let externalLink = article.externalLink, externalLink != article.preferredLink {
|
2021-09-17 14:10:33 -05:00
|
|
|
|
d["external_link_label"] = NSLocalizedString("Link:", comment: "Link")
|
|
|
|
|
d["external_link_stripped"] = externalLink.strippingHTTPOrHTTPSScheme
|
|
|
|
|
d["external_link"] = externalLink
|
2020-11-20 02:17:17 -06:00
|
|
|
|
} else {
|
2021-09-17 14:10:33 -05:00
|
|
|
|
d["external_link_label"] = ""
|
|
|
|
|
d["external_link_stripped"] = ""
|
2020-11-20 02:17:17 -06:00
|
|
|
|
d["external_link"] = ""
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-28 22:38:29 -08:00
|
|
|
|
d["body"] = body
|
2020-12-08 19:00:56 -06:00
|
|
|
|
|
|
|
|
|
#if os(macOS)
|
|
|
|
|
d["text_size_class"] = AppDefaults.shared.articleTextSize.cssClass
|
|
|
|
|
#endif
|
|
|
|
|
|
2020-02-08 17:21:55 -08:00
|
|
|
|
var components = URLComponents()
|
|
|
|
|
components.scheme = Self.imageIconScheme
|
|
|
|
|
components.path = article.articleID
|
|
|
|
|
if let imageIconURLString = components.string {
|
2021-09-16 17:11:16 -05:00
|
|
|
|
d["avatar_src"] = imageIconURLString
|
2020-02-08 17:21:55 -08:00
|
|
|
|
}
|
|
|
|
|
else {
|
2021-09-17 14:10:33 -05:00
|
|
|
|
d["avatar_src"] = ""
|
2020-02-08 17:21:55 -08:00
|
|
|
|
}
|
2020-08-19 15:44:40 -05:00
|
|
|
|
|
|
|
|
|
if self.title.isEmpty {
|
|
|
|
|
d["dateline_style"] = "articleDatelineTitle"
|
|
|
|
|
} else {
|
|
|
|
|
d["dateline_style"] = "articleDateline"
|
|
|
|
|
}
|
2017-12-29 17:10:50 -08:00
|
|
|
|
|
2024-02-25 23:12:21 -08:00
|
|
|
|
d["feed_link_title"] = article.feed?.nameForDisplay ?? ""
|
|
|
|
|
d["feed_link"] = article.feed?.homePageURL ?? ""
|
2017-12-29 11:31:47 -08:00
|
|
|
|
|
|
|
|
|
d["byline"] = byline()
|
|
|
|
|
|
2021-09-17 14:10:33 -05:00
|
|
|
|
let datePublished = article.logicalDatePublished
|
|
|
|
|
d["datetime_long"] = Self.longDateTimeFormatter.string(from: datePublished)
|
|
|
|
|
d["datetime_medium"] = Self.mediumDateTimeFormatter.string(from: datePublished)
|
|
|
|
|
d["datetime_short"] = Self.shortDateTimeFormatter.string(from: datePublished)
|
|
|
|
|
d["date_long"] = Self.longDateFormatter.string(from: datePublished)
|
|
|
|
|
d["date_medium"] = Self.mediumDateFormatter.string(from: datePublished)
|
|
|
|
|
d["date_short"] = Self.shortDateFormatter.string(from: datePublished)
|
|
|
|
|
d["time_long"] = Self.longTimeFormatter.string(from: datePublished)
|
|
|
|
|
d["time_medium"] = Self.mediumTimeFormatter.string(from: datePublished)
|
|
|
|
|
d["time_short"] = Self.shortTimeFormatter.string(from: datePublished)
|
|
|
|
|
|
2017-05-27 10:43:27 -07:00
|
|
|
|
return d
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-26 21:38:14 -08:00
|
|
|
|
func byline() -> String {
|
2024-02-25 23:12:21 -08:00
|
|
|
|
guard let authors = article?.authors ?? article?.feed?.authors, !authors.isEmpty else {
|
2017-12-29 11:31:47 -08:00
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-07 13:46:00 -05:00
|
|
|
|
// If the author's name is the same as the feed, then we don't want to display it.
|
|
|
|
|
// This code assumes that multiple authors would never match the feed name so that
|
|
|
|
|
// if there feed owner has an article co-author all authors are given the byline.
|
|
|
|
|
if authors.count == 1, let author = authors.first {
|
2024-02-25 23:12:21 -08:00
|
|
|
|
if author.name == article?.feed?.nameForDisplay {
|
2018-09-07 13:46:00 -05:00
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var byline = ""
|
2017-12-29 11:31:47 -08:00
|
|
|
|
var isFirstAuthor = true
|
|
|
|
|
|
|
|
|
|
for author in authors {
|
|
|
|
|
if !isFirstAuthor {
|
|
|
|
|
byline += ", "
|
|
|
|
|
}
|
|
|
|
|
isFirstAuthor = false
|
|
|
|
|
|
2020-08-18 17:44:28 -05:00
|
|
|
|
var authorEmailAddress: String? = nil
|
|
|
|
|
if let emailAddress = author.emailAddress, !(emailAddress.contains("noreply@") || emailAddress.contains("no-reply@")) {
|
|
|
|
|
authorEmailAddress = emailAddress
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let emailAddress = authorEmailAddress, emailAddress.contains(" ") {
|
2017-12-29 11:31:47 -08:00
|
|
|
|
byline += emailAddress // probably name plus email address
|
|
|
|
|
}
|
|
|
|
|
else if let name = author.name, let url = author.url {
|
2018-11-26 21:38:14 -08:00
|
|
|
|
byline += name.htmlByAddingLink(url)
|
2017-12-29 11:31:47 -08:00
|
|
|
|
}
|
2020-08-18 17:44:28 -05:00
|
|
|
|
else if let name = author.name, let emailAddress = authorEmailAddress {
|
2020-04-15 16:39:58 -05:00
|
|
|
|
byline += "\(name) <\(emailAddress)>"
|
2017-12-29 11:31:47 -08:00
|
|
|
|
}
|
|
|
|
|
else if let name = author.name {
|
|
|
|
|
byline += name
|
|
|
|
|
}
|
2020-08-18 17:44:28 -05:00
|
|
|
|
else if let emailAddress = authorEmailAddress {
|
2017-12-29 11:31:47 -08:00
|
|
|
|
byline += "<\(emailAddress)>" // TODO: mailto link
|
|
|
|
|
}
|
|
|
|
|
else if let url = author.url {
|
2018-11-26 21:38:14 -08:00
|
|
|
|
byline += String.htmlWithLink(url)
|
2017-12-29 11:31:47 -08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-12-29 17:10:50 -08:00
|
|
|
|
return byline
|
2017-12-29 11:31:47 -08:00
|
|
|
|
}
|
|
|
|
|
|
2020-03-12 18:07:40 -05:00
|
|
|
|
#if os(iOS)
|
|
|
|
|
func styleSubstitutions() -> [String: String] {
|
|
|
|
|
var d = [String: String]()
|
2020-03-12 19:01:10 -05:00
|
|
|
|
let bodyFont = UIFont.preferredFont(forTextStyle: .body)
|
|
|
|
|
d["font-size"] = String(describing: bodyFont.pointSize)
|
2020-03-12 18:07:40 -05:00
|
|
|
|
return d
|
|
|
|
|
}
|
2020-03-20 06:41:38 -05:00
|
|
|
|
#else
|
|
|
|
|
func styleSubstitutions() -> [String: String] {
|
2020-12-08 19:00:56 -06:00
|
|
|
|
return [String: String]()
|
2020-03-20 06:41:38 -05:00
|
|
|
|
}
|
2020-03-12 18:07:40 -05:00
|
|
|
|
#endif
|
2018-11-26 21:38:14 -08:00
|
|
|
|
|
2017-05-27 10:43:27 -07:00
|
|
|
|
}
|
2019-04-14 12:54:17 -07:00
|
|
|
|
|
|
|
|
|
// MARK: - Article extension
|
|
|
|
|
|
2024-03-19 23:05:30 -07:00
|
|
|
|
@MainActor private extension Article {
|
2019-04-14 12:54:17 -07:00
|
|
|
|
|
|
|
|
|
var baseURL: URL? {
|
2021-09-30 16:46:11 +13:00
|
|
|
|
var s = link
|
2019-04-14 12:54:17 -07:00
|
|
|
|
if s == nil {
|
2024-02-25 23:12:21 -08:00
|
|
|
|
s = feed?.homePageURL
|
2019-04-14 12:54:17 -07:00
|
|
|
|
}
|
|
|
|
|
if s == nil {
|
2024-02-25 23:12:21 -08:00
|
|
|
|
s = feed?.url
|
2019-04-14 12:54:17 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
guard let urlString = s else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
var urlComponents = URLComponents(string: urlString)
|
|
|
|
|
if urlComponents == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Can’t use url-with-fragment as base URL. The webview won’t load. See scripting.com/rss.xml for example.
|
|
|
|
|
urlComponents!.fragment = nil
|
|
|
|
|
guard let url = urlComponents!.url, url.scheme == "http" || url.scheme == "https" else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return url
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|