Page MenuHomeMusing Studio

No OneTemporary

diff --git a/Shared/Account/AccountLogoutView.swift b/Shared/Account/AccountLogoutView.swift
index 22df2da..5f087a8 100644
--- a/Shared/Account/AccountLogoutView.swift
+++ b/Shared/Account/AccountLogoutView.swift
@@ -1,80 +1,80 @@
import SwiftUI
struct AccountLogoutView: View {
@EnvironmentObject var model: WriteFreelyModel
@State private var isPresentingLogoutConfirmation: Bool = false
@State private var editedPostsWarningString: String = ""
var body: some View {
#if os(iOS)
VStack {
Spacer()
VStack {
Text("Logged in as \(model.account.username)")
Text("on \(model.account.server)")
}
Spacer()
Button(action: logoutHandler, label: {
Text("Log Out")
})
}
.actionSheet(isPresented: $isPresentingLogoutConfirmation, content: {
ActionSheet(
title: Text("Log Out?"),
message: Text("\(editedPostsWarningString)You won't lose any local posts. Are you sure?"),
buttons: [
.destructive(Text("Log Out"), action: {
model.logout()
}),
.cancel()
]
)
})
#else
VStack {
Spacer()
VStack {
Text("Logged in as \(model.account.username)")
Text("on \(model.account.server)")
}
Spacer()
Button(action: logoutHandler, label: {
Text("Log Out")
})
}
.alert(isPresented: $isPresentingLogoutConfirmation) {
Alert(
title: Text("Log Out?"),
message: Text("\(editedPostsWarningString)You won't lose any local posts. Are you sure?"),
primaryButton: .cancel(Text("Cancel"), action: { self.isPresentingLogoutConfirmation = false }),
secondaryButton: .destructive(Text("Log Out"), action: model.logout )
)
}
#endif
}
func logoutHandler() {
let request = WFAPost.createFetchRequest()
request.predicate = NSPredicate(format: "status == %i", 1)
do {
let editedPosts = try LocalStorageManager.standard.container.viewContext.fetch(request)
if editedPosts.count == 1 {
editedPostsWarningString = "You'll lose unpublished changes to \(editedPosts.count) edited post. "
}
if editedPosts.count > 1 {
editedPostsWarningString = "You'll lose unpublished changes to \(editedPosts.count) edited posts. "
}
} catch {
- print("Error: failed to fetch cached posts")
+ fatalError("Error: failed to fetch cached posts")
}
self.isPresentingLogoutConfirmation = true
}
}
struct AccountLogoutView_Previews: PreviewProvider {
static var previews: some View {
AccountLogoutView()
.environmentObject(WriteFreelyModel())
}
}
diff --git a/Shared/Extensions/WriteFreelyModel+API.swift b/Shared/Extensions/WriteFreelyModel+API.swift
index 939cf76..01edabc 100644
--- a/Shared/Extensions/WriteFreelyModel+API.swift
+++ b/Shared/Extensions/WriteFreelyModel+API.swift
@@ -1,152 +1,164 @@
import Foundation
import WriteFreely
extension WriteFreelyModel {
func login(to server: URL, as username: String, password: String) {
if !hasNetworkConnection {
isPresentingNetworkErrorAlert = true
return
}
let secureProtocolPrefix = "https://"
let insecureProtocolPrefix = "http://"
var serverString = server.absoluteString
// If there's neither an http or https prefix, prepend "https://" to the server string.
if !(serverString.hasPrefix(secureProtocolPrefix) || serverString.hasPrefix(insecureProtocolPrefix)) {
serverString = secureProtocolPrefix + serverString
}
// If the server string is prefixed with http, upgrade to https before attempting to login.
if serverString.hasPrefix(insecureProtocolPrefix) {
serverString = serverString.replacingOccurrences(of: insecureProtocolPrefix, with: secureProtocolPrefix)
}
isLoggingIn = true
var serverURL = URL(string: serverString)!
if !serverURL.path.isEmpty {
serverURL.deleteLastPathComponent()
}
account.server = serverURL.absoluteString
client = WFClient(for: serverURL)
client?.login(username: username, password: password, completion: loginHandler)
}
func logout() {
if !hasNetworkConnection {
DispatchQueue.main.async { self.isPresentingNetworkErrorAlert = true }
return
}
guard let loggedInClient = client else {
do {
try purgeTokenFromKeychain(username: account.username, server: account.server)
account.logout()
} catch {
fatalError("Failed to log out persisted state")
}
return
}
loggedInClient.logout(completion: logoutHandler)
}
func fetchUserCollections() {
if !hasNetworkConnection {
DispatchQueue.main.async { self.isPresentingNetworkErrorAlert = true }
return
}
- guard let loggedInClient = client else { return }
+ guard let loggedInClient = client else {
+ fatalError("Could not get logged in client")
+ }
// We're starting the network request.
DispatchQueue.main.async {
self.isProcessingRequest = true
}
loggedInClient.getUserCollections(completion: fetchUserCollectionsHandler)
}
func fetchUserPosts() {
if !hasNetworkConnection {
DispatchQueue.main.async { self.isPresentingNetworkErrorAlert = true }
return
}
- guard let loggedInClient = client else { return }
+ guard let loggedInClient = client else {
+ fatalError("Could not get logged in client")
+ }
// We're starting the network request.
DispatchQueue.main.async {
self.isProcessingRequest = true
}
loggedInClient.getPosts(completion: fetchUserPostsHandler)
}
func publish(post: WFAPost) {
postToUpdate = nil
if !hasNetworkConnection {
DispatchQueue.main.async { self.isPresentingNetworkErrorAlert = true }
return
}
- guard let loggedInClient = client else { return }
+ guard let loggedInClient = client else {
+ fatalError("Could not get logged in client")
+ }
// We're starting the network request.
DispatchQueue.main.async {
self.isProcessingRequest = true
}
if post.language == nil {
if let languageCode = Locale.current.languageCode {
post.language = languageCode
post.rtl = Locale.characterDirection(forLanguage: languageCode) == .rightToLeft
}
}
var wfPost = WFPost(
body: post.body,
title: post.title.isEmpty ? "" : post.title,
appearance: post.appearance,
language: post.language,
rtl: post.rtl,
createdDate: post.status == PostStatus.local.rawValue ? Date() : post.createdDate
)
if let existingPostId = post.postId {
// This is an existing post.
postToUpdate = post
wfPost.postId = post.postId
loggedInClient.updatePost(
postId: existingPostId,
updatedPost: wfPost,
completion: publishHandler
)
} else {
// This is a new local draft.
loggedInClient.createPost(
post: wfPost, in: post.collectionAlias, completion: publishHandler
)
}
}
func updateFromServer(post: WFAPost) {
if !hasNetworkConnection {
DispatchQueue.main.async { self.isPresentingNetworkErrorAlert = true }
return
}
- guard let loggedInClient = client else { return }
- guard let postId = post.postId else { return }
+ guard let loggedInClient = client else {
+ fatalError("Could not get logged in client")
+ }
+ guard let postId = post.postId else {
+ fatalError("Could not get post ID")
+ }
// We're starting the network request.
DispatchQueue.main.async {
self.selectedPost = post
self.isProcessingRequest = true
}
loggedInClient.getPost(byId: postId, completion: updateFromServerHandler)
}
func move(post: WFAPost, from oldCollection: WFACollection?, to newCollection: WFACollection?) {
if !hasNetworkConnection {
DispatchQueue.main.async { self.isPresentingNetworkErrorAlert = true }
return
}
guard let loggedInClient = client,
- let postId = post.postId else { return }
+ let postId = post.postId else {
+ fatalError("Could not get post ID")
+ }
// We're starting the network request.
DispatchQueue.main.async {
self.isProcessingRequest = true
}
selectedPost = post
post.collectionAlias = newCollection?.alias
loggedInClient.movePost(postId: postId, to: newCollection?.alias, completion: movePostHandler)
}
}
diff --git a/Shared/Extensions/WriteFreelyModel+APIHandlers.swift b/Shared/Extensions/WriteFreelyModel+APIHandlers.swift
index 804dd41..8b1e5d0 100644
--- a/Shared/Extensions/WriteFreelyModel+APIHandlers.swift
+++ b/Shared/Extensions/WriteFreelyModel+APIHandlers.swift
@@ -1,278 +1,284 @@
import Foundation
import WriteFreely
extension WriteFreelyModel {
func loginHandler(result: Result<WFUser, Error>) {
DispatchQueue.main.async {
self.isLoggingIn = false
}
do {
let user = try result.get()
fetchUserCollections()
fetchUserPosts()
do {
try saveTokenToKeychain(user.token, username: user.username, server: account.server)
DispatchQueue.main.async {
self.account.login(user)
}
} catch {
DispatchQueue.main.async {
self.loginErrorMessage = "There was a problem storing your access token to the Keychain."
self.isPresentingLoginErrorAlert = true
}
}
} catch WFError.notFound {
DispatchQueue.main.async {
self.loginErrorMessage = AccountError.usernameNotFound.localizedDescription
self.isPresentingLoginErrorAlert = true
}
} catch WFError.unauthorized {
DispatchQueue.main.async {
self.loginErrorMessage = AccountError.invalidPassword.localizedDescription
self.isPresentingLoginErrorAlert = true
}
} catch {
if (error as NSError).domain == NSURLErrorDomain,
(error as NSError).code == -1003 {
DispatchQueue.main.async {
self.loginErrorMessage = AccountError.serverNotFound.localizedDescription
self.isPresentingLoginErrorAlert = true
}
} else {
DispatchQueue.main.async {
self.loginErrorMessage = error.localizedDescription
self.isPresentingLoginErrorAlert = true
}
}
}
}
func logoutHandler(result: Result<Bool, Error>) {
do {
_ = try result.get()
do {
try purgeTokenFromKeychain(username: account.user?.username, server: account.server)
client = nil
DispatchQueue.main.async {
self.account.logout()
LocalStorageManager.standard.purgeUserCollections()
self.posts.purgePublishedPosts()
}
} catch {
- print("Something went wrong purging the token from the Keychain.")
+ fatalError("Something went wrong purging the token from the Keychain.")
}
} catch WFError.notFound {
// The user token is invalid or doesn't exist, so it's been invalidated by the server. Proceed with
// purging the token from the Keychain, destroying the client object, and setting the AccountModel to its
// logged-out state.
do {
try purgeTokenFromKeychain(username: account.user?.username, server: account.server)
client = nil
DispatchQueue.main.async {
self.account.logout()
LocalStorageManager.standard.purgeUserCollections()
self.posts.purgePublishedPosts()
}
} catch {
- print("Something went wrong purging the token from the Keychain.")
+ fatalError("Something went wrong purging the token from the Keychain.")
}
} catch {
// We get a 'cannot parse response' (similar to what we were seeing in the Swift package) NSURLError here,
// so we're using a hacky workaround — if we get the NSURLError, but the AccountModel still thinks we're
// logged in, try calling the logout function again and see what we get.
// Conditional cast from 'Error' to 'NSError' always succeeds but is the only way to check error properties.
if (error as NSError).domain == NSURLErrorDomain,
(error as NSError).code == NSURLErrorCannotParseResponse {
if account.isLoggedIn {
self.logout()
}
}
}
}
func fetchUserCollectionsHandler(result: Result<[WFCollection], Error>) {
// We're done with the network request.
DispatchQueue.main.async {
self.isProcessingRequest = false
}
do {
let fetchedCollections = try result.get()
for fetchedCollection in fetchedCollections {
DispatchQueue.main.async {
let localCollection = WFACollection(context: LocalStorageManager.standard.container.viewContext)
localCollection.alias = fetchedCollection.alias
localCollection.blogDescription = fetchedCollection.description
localCollection.email = fetchedCollection.email
localCollection.isPublic = fetchedCollection.isPublic ?? false
localCollection.styleSheet = fetchedCollection.styleSheet
localCollection.title = fetchedCollection.title
localCollection.url = fetchedCollection.url
}
}
DispatchQueue.main.async {
LocalStorageManager.standard.saveContext()
}
} catch WFError.unauthorized {
DispatchQueue.main.async {
self.loginErrorMessage = "Something went wrong, please try logging in again."
self.isPresentingLoginErrorAlert = true
}
self.logout()
} catch {
- print(error)
+ fatalError(error.localizedDescription)
}
}
func fetchUserPostsHandler(result: Result<[WFPost], Error>) {
// We're done with the network request.
DispatchQueue.main.async {
self.isProcessingRequest = false
}
let request = WFAPost.createFetchRequest()
do {
let locallyCachedPosts = try LocalStorageManager.standard.container.viewContext.fetch(request)
do {
var postsToDelete = locallyCachedPosts.filter { $0.status != PostStatus.local.rawValue }
let fetchedPosts = try result.get()
for fetchedPost in fetchedPosts {
if let managedPost = locallyCachedPosts.first(where: { $0.postId == fetchedPost.postId }) {
DispatchQueue.main.async {
managedPost.wasDeletedFromServer = false
if let fetchedPostUpdatedDate = fetchedPost.updatedDate,
let localPostUpdatedDate = managedPost.updatedDate {
managedPost.hasNewerRemoteCopy = fetchedPostUpdatedDate > localPostUpdatedDate
- } else { print("Error: could not determine which copy of post is newer") }
+ } else {
+ fatalError("Error: could not determine which copy of post is newer")
+ }
postsToDelete.removeAll(where: { $0.postId == fetchedPost.postId })
}
} else {
DispatchQueue.main.async {
let managedPost = WFAPost(context: LocalStorageManager.standard.container.viewContext)
self.importData(from: fetchedPost, into: managedPost)
managedPost.collectionAlias = fetchedPost.collectionAlias
managedPost.wasDeletedFromServer = false
}
}
}
DispatchQueue.main.async {
for post in postsToDelete { post.wasDeletedFromServer = true }
LocalStorageManager.standard.saveContext()
}
} catch {
- print(error)
+ fatalError(error.localizedDescription)
}
} catch WFError.unauthorized {
DispatchQueue.main.async {
self.loginErrorMessage = "Something went wrong, please try logging in again."
self.isPresentingLoginErrorAlert = true
}
self.logout()
} catch {
- print("Error: Failed to fetch cached posts")
+ fatalError("Error: Failed to fetch cached posts")
}
}
func publishHandler(result: Result<WFPost, Error>) {
// We're done with the network request.
DispatchQueue.main.async {
self.isProcessingRequest = false
}
// ⚠️ NOTE:
// The API does not return a collection alias, so we take care not to overwrite the
// cached post's collection alias with the 'nil' value from the fetched post.
// See: https://github.com/writeas/writefreely-swift/issues/20
do {
let fetchedPost = try result.get()
// If this is an updated post, check it against postToUpdate.
if let updatingPost = self.postToUpdate {
importData(from: fetchedPost, into: updatingPost)
DispatchQueue.main.async {
LocalStorageManager.standard.saveContext()
}
} else {
// Otherwise if it's a newly-published post, find it in the local store.
let request = WFAPost.createFetchRequest()
let matchBodyPredicate = NSPredicate(format: "body == %@", fetchedPost.body)
if let fetchedPostTitle = fetchedPost.title {
let matchTitlePredicate = NSPredicate(format: "title == %@", fetchedPostTitle)
request.predicate = NSCompoundPredicate(
andPredicateWithSubpredicates: [
matchTitlePredicate,
matchBodyPredicate
]
)
} else {
request.predicate = matchBodyPredicate
}
do {
let cachedPostsResults = try LocalStorageManager.standard.container.viewContext.fetch(request)
- guard let cachedPost = cachedPostsResults.first else { return }
+ guard let cachedPost = cachedPostsResults.first else {
+ fatalError("Could not get cached post from results")
+ }
importData(from: fetchedPost, into: cachedPost)
DispatchQueue.main.async {
LocalStorageManager.standard.saveContext()
}
} catch {
- print("Error: Failed to fetch cached posts")
+ fatalError("Error: Failed to fetch cached posts")
}
}
} catch {
- print(error)
+ fatalError(error.localizedDescription)
}
}
func updateFromServerHandler(result: Result<WFPost, Error>) {
// We're done with the network request.
DispatchQueue.main.async {
self.isProcessingRequest = false
}
// ⚠️ NOTE:
// The API does not return a collection alias, so we take care not to overwrite the
// cached post's collection alias with the 'nil' value from the fetched post.
// See: https://github.com/writeas/writefreely-swift/issues/20
do {
let fetchedPost = try result.get()
- guard let cachedPost = self.selectedPost else { return }
+ guard let cachedPost = self.selectedPost else {
+ fatalError("Could not get cached post")
+ }
importData(from: fetchedPost, into: cachedPost)
cachedPost.hasNewerRemoteCopy = false
DispatchQueue.main.async {
LocalStorageManager.standard.saveContext()
}
} catch {
- print(error)
+ fatalError(error.localizedDescription)
}
}
func movePostHandler(result: Result<Bool, Error>) {
// We're done with the network request.
DispatchQueue.main.async {
self.isProcessingRequest = false
}
do {
let succeeded = try result.get()
if succeeded {
if let post = selectedPost {
updateFromServer(post: post)
} else {
- return
+ fatalError("Could not update post from server")
}
}
} catch {
DispatchQueue.main.async {
LocalStorageManager.standard.container.viewContext.rollback()
}
- print(error)
+ fatalError(error.localizedDescription)
}
}
private func importData(from fetchedPost: WFPost, into cachedPost: WFAPost) {
cachedPost.appearance = fetchedPost.appearance
cachedPost.body = fetchedPost.body
cachedPost.createdDate = fetchedPost.createdDate
cachedPost.language = fetchedPost.language
cachedPost.postId = fetchedPost.postId
cachedPost.rtl = fetchedPost.rtl ?? false
cachedPost.slug = fetchedPost.slug
cachedPost.status = PostStatus.published.rawValue
cachedPost.title = fetchedPost.title ?? ""
cachedPost.updatedDate = fetchedPost.updatedDate
}
}
diff --git a/Shared/LocalStorageManager.swift b/Shared/LocalStorageManager.swift
index b644faf..409f8c3 100644
--- a/Shared/LocalStorageManager.swift
+++ b/Shared/LocalStorageManager.swift
@@ -1,123 +1,123 @@
import CoreData
#if os(iOS)
import UIKit
#elseif os(macOS)
import AppKit
#endif
final class LocalStorageManager {
public static var standard = LocalStorageManager()
public let container: NSPersistentContainer
private let containerName = "LocalStorageModel"
private init() {
container = NSPersistentContainer(name: containerName)
setupStore(in: container)
registerObservers()
}
func saveContext() {
if container.viewContext.hasChanges {
do {
try container.viewContext.save()
} catch {
- print("Error saving context: \(error)")
+ fatalError("Error saving context: \(error)")
}
}
}
func purgeUserCollections() {
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "WFACollection")
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
do {
try container.viewContext.executeAndMergeChanges(using: deleteRequest)
} catch {
- print("Error: Failed to purge cached collections.")
+ fatalError("Error: Failed to purge cached collections.")
}
}
}
private extension LocalStorageManager {
var oldStoreURL: URL {
let appSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
return appSupport.appendingPathComponent("LocalStorageModel.sqlite")
}
var sharedStoreURL: URL {
let id = "group.com.abunchtell.writefreely"
let groupContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: id)!
return groupContainer.appendingPathComponent("LocalStorageModel.sqlite")
}
func setupStore(in container: NSPersistentContainer) {
if !FileManager.default.fileExists(atPath: oldStoreURL.path) {
container.persistentStoreDescriptions.first!.url = sharedStoreURL
}
container.loadPersistentStores { _, error in
if let error = error {
fatalError("Core Data store failed to load with error: \(error)")
}
}
migrateStore(for: container)
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
}
func migrateStore(for container: NSPersistentContainer) {
// Check if the shared store exists before attempting a migration — for example, in case we've already attempted
// and successfully completed a migration, but the deletion of the old store failed for some reason.
guard !FileManager.default.fileExists(atPath: sharedStoreURL.path) else { return }
let coordinator = container.persistentStoreCoordinator
// Get a reference to the old store.
guard let oldStore = coordinator.persistentStore(for: oldStoreURL) else {
return
}
// Attempt to migrate the old store over to the shared store URL.
do {
try coordinator.migratePersistentStore(oldStore,
to: sharedStoreURL,
options: nil,
withType: NSSQLiteStoreType)
} catch {
fatalError("Something went wrong migrating the store: \(error)")
}
// Attempt to delete the old store.
do {
try FileManager.default.removeItem(at: oldStoreURL)
} catch {
fatalError("Something went wrong while deleting the old store: \(error)")
}
}
func registerObservers() {
let center = NotificationCenter.default
#if os(iOS)
let notification = UIApplication.willResignActiveNotification
#elseif os(macOS)
let notification = NSApplication.willResignActiveNotification
#endif
// We don't need to worry about removing this observer because we're targeting iOS 9+ / macOS 10.11+; the
// system will clean this up the next time it would be posted to.
// See: https://developer.apple.com/documentation/foundation/notificationcenter/1413994-removeobserver
// And: https://developer.apple.com/documentation/foundation/notificationcenter/1407263-removeobserver
// swiftlint:disable:next discarded_notification_center_observer
center.addObserver(forName: notification, object: nil, queue: nil, using: self.saveContextOnResignActive)
}
func saveContextOnResignActive(_ notification: Notification) {
saveContext()
}
}
diff --git a/Shared/PostCollection/CollectionListModel.swift b/Shared/PostCollection/CollectionListModel.swift
index 5e107bb..8beae45 100644
--- a/Shared/PostCollection/CollectionListModel.swift
+++ b/Shared/PostCollection/CollectionListModel.swift
@@ -1,40 +1,40 @@
import SwiftUI
import CoreData
class CollectionListModel: NSObject, ObservableObject {
@Published var list: [WFACollection] = []
private let collectionsController: NSFetchedResultsController<WFACollection>
init(managedObjectContext: NSManagedObjectContext) {
collectionsController = NSFetchedResultsController(fetchRequest: WFACollection.collectionsFetchRequest,
managedObjectContext: managedObjectContext,
sectionNameKeyPath: nil,
cacheName: nil)
super.init()
collectionsController.delegate = self
do {
try collectionsController.performFetch()
list = collectionsController.fetchedObjects ?? []
} catch {
- print("Failed to fetch collections!")
+ fatalError("Failed to fetch collections!")
}
}
}
extension CollectionListModel: NSFetchedResultsControllerDelegate {
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
guard let collections = controller.fetchedObjects as? [WFACollection] else { return }
self.list = collections
}
}
extension WFACollection {
static var collectionsFetchRequest: NSFetchRequest<WFACollection> {
let request: NSFetchRequest<WFACollection> = WFACollection.createFetchRequest()
request.sortDescriptors = [NSSortDescriptor(keyPath: \WFACollection.title, ascending: true)]
return request
}
}
diff --git a/Shared/PostList/PostListModel.swift b/Shared/PostList/PostListModel.swift
index db0ff4a..4cf62b0 100644
--- a/Shared/PostList/PostListModel.swift
+++ b/Shared/PostList/PostListModel.swift
@@ -1,126 +1,126 @@
import SwiftUI
import CoreData
class PostListModel: ObservableObject {
func remove(_ post: WFAPost) {
withAnimation {
LocalStorageManager.standard.container.viewContext.delete(post)
LocalStorageManager.standard.saveContext()
}
}
func purgePublishedPosts() {
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "WFAPost")
fetchRequest.predicate = NSPredicate(format: "status != %i", 0)
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
do {
try LocalStorageManager.standard.container.viewContext.executeAndMergeChanges(using: deleteRequest)
} catch {
- print("Error: Failed to purge cached posts.")
+ fatalError("Error: Failed to purge cached posts.")
}
}
func getBodyPreview(of post: WFAPost) -> String {
var elidedPostBody: String = ""
// Strip any markdown from the post body.
let strippedPostBody = stripMarkdown(from: post.body)
// Extract lede from post.
elidedPostBody = extractLede(from: strippedPostBody)
return elidedPostBody
}
}
private extension PostListModel {
func stripMarkdown(from string: String) -> String {
var strippedString = string
strippedString = stripHeadingOctothorpes(from: strippedString)
strippedString = stripImages(from: strippedString, keepAltText: true)
return strippedString
}
func stripHeadingOctothorpes(from string: String) -> String {
let newLines = CharacterSet.newlines
var processedComponents: [String] = []
let components = string.components(separatedBy: newLines)
for component in components {
if component.isEmpty {
continue
}
var newString = component
while newString.first == "#" {
newString.removeFirst()
}
if newString.hasPrefix(" ") {
newString.removeFirst()
}
processedComponents.append(newString)
}
let headinglessString = processedComponents.joined(separator: "\n\n")
return headinglessString
}
func stripImages(from string: String, keepAltText: Bool = false) -> String {
let pattern = #"!\[[\"]?(.*?)[\"|]?\]\(.*?\)"#
var processedComponents: [String] = []
let components = string.components(separatedBy: .newlines)
for component in components {
if component.isEmpty { continue }
var processedString: String = component
if keepAltText {
let regex = try? NSRegularExpression(pattern: pattern, options: [])
if let matches = regex?.matches(
in: component, options: [], range: NSRange(location: 0, length: component.utf16.count)
) {
for match in matches {
if let range = Range(match.range(at: 1), in: component) {
processedString = "\(component[range])"
}
}
}
} else {
let range = component.startIndex..<component.endIndex
processedString = component.replacingOccurrences(
of: pattern,
with: "",
options: .regularExpression,
range: range
)
}
if processedString.isEmpty { continue }
processedComponents.append(processedString)
}
return processedComponents.joined(separator: "\n\n")
}
func extractLede(from string: String) -> String {
let truncatedString = string.prefix(80)
let terminatingPunctuation = ".。?"
let terminatingCharacters = CharacterSet(charactersIn: terminatingPunctuation).union(.newlines)
var lede: String = ""
let sentences = truncatedString.components(separatedBy: terminatingCharacters)
if let firstSentence = (sentences.filter { !$0.isEmpty }).first {
if truncatedString.count > firstSentence.count {
if terminatingPunctuation.contains(truncatedString[firstSentence.endIndex]) {
lede = String(truncatedString[...firstSentence.endIndex])
} else {
lede = firstSentence
}
} else if truncatedString.count == firstSentence.count {
if string.count > 80 {
if let endOfStringIndex = truncatedString.lastIndex(of: " ") {
lede = truncatedString[..<endOfStringIndex] + "…"
}
} else {
lede = firstSentence
}
}
}
return lede
}
}

File Metadata

Mime Type
text/x-diff
Expires
Sun, May 17, 8:02 PM (1 d, 14 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3733094

Event Timeline