diff --git a/Shared/PostList/PostCellView.swift b/Shared/PostList/PostCellView.swift index 14fa2fc..1522141 100644 --- a/Shared/PostList/PostCellView.swift +++ b/Shared/PostList/PostCellView.swift @@ -1,64 +1,86 @@ import SwiftUI struct PostCellView: View { + @EnvironmentObject var model: WriteFreelyModel @ObservedObject var post: WFAPost var collectionName: String? static let createdDateFormat: DateFormatter = { let formatter = DateFormatter() formatter.locale = Locale.current formatter.dateStyle = .long formatter.timeStyle = .short return formatter }() + var titleText: String { + if post.title.isEmpty { + return model.posts.getBodyPreview(of: post) + } + return post.title + } + var body: some View { HStack { VStack(alignment: .leading) { if let collectionName = collectionName { Text(collectionName) .font(.caption) .foregroundColor(.secondary) .padding(EdgeInsets(top: 3, leading: 4, bottom: 3, trailing: 4)) .overlay(RoundedRectangle(cornerRadius: 2).stroke(Color.secondary, lineWidth: 1)) } - Text(post.title) + Text(titleText) .font(.headline) Text(post.createdDate ?? Date(), formatter: Self.createdDateFormat) .font(.caption) .foregroundColor(.secondary) .padding(.top, -3) } Spacer() PostStatusBadgeView(post: post) } .padding(5) } } struct PostCell_AllPostsPreviews: PreviewProvider { static var previews: some View { let context = LocalStorageManager.persistentContainer.viewContext let testPost = WFAPost(context: context) testPost.title = "Test Post Title" testPost.body = "Here's some cool sample body text." testPost.createdDate = Date() return PostCellView(post: testPost, collectionName: "My Cool Blog") .environment(\.managedObjectContext, context) } } struct PostCell_NormalPreviews: PreviewProvider { static var previews: some View { let context = LocalStorageManager.persistentContainer.viewContext let testPost = WFAPost(context: context) testPost.title = "Test Post Title" testPost.body = "Here's some cool sample body text." testPost.collectionAlias = "My Cool Blog" testPost.createdDate = Date() return PostCellView(post: testPost) .environment(\.managedObjectContext, context) } } + +struct PostCell_NoTitlePreviews: PreviewProvider { + static var previews: some View { + let context = LocalStorageManager.persistentContainer.viewContext + let testPost = WFAPost(context: context) + testPost.title = "" + testPost.body = "Here's some cool sample body text." + testPost.collectionAlias = "My Cool Blog" + testPost.createdDate = Date() + + return PostCellView(post: testPost) + .environment(\.managedObjectContext, context) + } +} diff --git a/Shared/PostList/PostListModel.swift b/Shared/PostList/PostListModel.swift index e6464e4..98e158b 100644 --- a/Shared/PostList/PostListModel.swift +++ b/Shared/PostList/PostListModel.swift @@ -1,21 +1,124 @@ import SwiftUI import CoreData class PostListModel: ObservableObject { func remove(_ post: WFAPost) { LocalStorageManager.persistentContainer.viewContext.delete(post) LocalStorageManager().saveContext() } func purgePublishedPosts() { let fetchRequest: NSFetchRequest = NSFetchRequest(entityName: "WFAPost") fetchRequest.predicate = NSPredicate(format: "status != %i", 0) let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) do { try LocalStorageManager.persistentContainer.viewContext.executeAndMergeChanges(using: deleteRequest) } catch { print("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.. 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[..