From 8cd6f107e5c927049b3a599b57f60d14fedca44a Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 18 Sep 2019 18:15:55 -0500 Subject: [PATCH] Add basic Article Content extraction --- Mac/Base.lproj/MainWindow.storyboard | 25 +++- Mac/MainWindow/ArticleExtractorButton.swift | 109 ++++++++++++++++++ .../Detail/DetailViewController.swift | 1 + .../Detail/DetailWebViewController.swift | 2 + Mac/MainWindow/MainWindowController.swift | 87 ++++++++++++++ .../articleExtractor.imageset/Contents.json | 12 ++ .../articleExtractor.imageset/FullArticle.pdf | Bin 0 -> 4081 bytes .../Contents.json | 12 ++ .../FullArticleError.png | Bin 0 -> 282 bytes .../Contents.json | 12 ++ .../FullArticleProgress1.pdf | Bin 0 -> 4105 bytes .../Contents.json | 12 ++ .../FullArticleProgress2.pdf | Bin 0 -> 4101 bytes .../Contents.json | 12 ++ .../FullArticleProgress3.pdf | Bin 0 -> 4102 bytes .../Contents.json | 12 ++ .../FullArticleProgress4.pdf | Bin 0 -> 4102 bytes NetNewsWire.xcodeproj/project.pbxproj | 30 +++++ .../xcschemes/NetNewsWire.xcscheme | 15 ++- .../Article Extractor/ArticleExtractor.swift | 102 ++++++++++++++++ .../ArticleExtractorConfig.swift | 35 ++++++ .../Article Extractor/ExtractedArticle.swift | 45 ++++++++ .../Article Rendering/ArticleRenderer.swift | 25 ++-- submodules/RSCore | 2 +- 24 files changed, 534 insertions(+), 16 deletions(-) create mode 100644 Mac/MainWindow/ArticleExtractorButton.swift create mode 100644 Mac/Resources/Assets.xcassets/articleExtractor.imageset/Contents.json create mode 100644 Mac/Resources/Assets.xcassets/articleExtractor.imageset/FullArticle.pdf create mode 100644 Mac/Resources/Assets.xcassets/articleExtractorError.imageset/Contents.json create mode 100644 Mac/Resources/Assets.xcassets/articleExtractorError.imageset/FullArticleError.png create mode 100644 Mac/Resources/Assets.xcassets/articleExtractorProgress1.imageset/Contents.json create mode 100644 Mac/Resources/Assets.xcassets/articleExtractorProgress1.imageset/FullArticleProgress1.pdf create mode 100644 Mac/Resources/Assets.xcassets/articleExtractorProgress2.imageset/Contents.json create mode 100644 Mac/Resources/Assets.xcassets/articleExtractorProgress2.imageset/FullArticleProgress2.pdf create mode 100644 Mac/Resources/Assets.xcassets/articleExtractorProgress3.imageset/Contents.json create mode 100644 Mac/Resources/Assets.xcassets/articleExtractorProgress3.imageset/FullArticleProgress3.pdf create mode 100644 Mac/Resources/Assets.xcassets/articleExtractorProgress4.imageset/Contents.json create mode 100644 Mac/Resources/Assets.xcassets/articleExtractorProgress4.imageset/FullArticleProgress4.pdf create mode 100644 Shared/Article Extractor/ArticleExtractor.swift create mode 100644 Shared/Article Extractor/ArticleExtractorConfig.swift create mode 100644 Shared/Article Extractor/ExtractedArticle.swift diff --git a/Mac/Base.lproj/MainWindow.storyboard b/Mac/Base.lproj/MainWindow.storyboard index 0a9cf634e..bb8b468c1 100644 --- a/Mac/Base.lproj/MainWindow.storyboard +++ b/Mac/Base.lproj/MainWindow.storyboard @@ -1,8 +1,8 @@ - + - + @@ -168,6 +168,22 @@ + + + + + + + + + @@ -180,6 +196,7 @@ + @@ -192,6 +209,7 @@ + @@ -276,7 +294,7 @@ - + @@ -488,6 +506,7 @@ + diff --git a/Mac/MainWindow/ArticleExtractorButton.swift b/Mac/MainWindow/ArticleExtractorButton.swift new file mode 100644 index 000000000..937cb70c3 --- /dev/null +++ b/Mac/MainWindow/ArticleExtractorButton.swift @@ -0,0 +1,109 @@ +// +// ArticleExtractorButton.swift +// NetNewsWire +// +// Created by Maurice Parker on 9/18/19. +// Copyright © 2019 Ranchero Software. All rights reserved. +// + +import Foundation + +class ArticleExtractorButton: NSButton { + + var isError = false { + didSet { + needsDisplay = true + } + } + + var isInProgress = false { + didSet { + needsDisplay = true + } + } + + override init(frame frameRect: NSRect) { + super.init(frame: frameRect) + wantsLayer = true + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + wantsLayer = true + } + + override func draw(_ dirtyRect: NSRect) { + + super.draw(dirtyRect) + + guard let hostedLayer = self.layer else { + return + } + + if let imageLayer = hostedLayer.sublayers?[0] { + if needsToDraw(imageLayer.bounds) { + imageLayer.removeFromSuperlayer() + } else { + return + } + } + + let opacity: Float = isEnabled ? 1.0 : 0.5 + + switch true { + case isError: + addImageSublayer(to: hostedLayer, imageName: "articleExtractorError", opacity: opacity) + case isInProgress: + addProgressSublayer(to: hostedLayer) + default: + addImageSublayer(to: hostedLayer, imageName: "articleExtractor", opacity: opacity) + } + + } + + private func makeLayerForImage(_ image: NSImage) -> CALayer { + let imageLayer = CALayer() + imageLayer.bounds = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height) + imageLayer.position = CGPoint(x: bounds.midX, y: bounds.midY) + return imageLayer + } + + private func addImageSublayer(to hostedLayer: CALayer, imageName: String, opacity: Float = 1.0) { + + guard let image = NSImage(named: imageName) else { + fatalError("Image doesn't exist: \(imageName)") + } + + let imageLayer = makeLayerForImage(image) + imageLayer.contents = image + imageLayer.opacity = opacity + hostedLayer.addSublayer(imageLayer) + + } + + private func addProgressSublayer(to hostedLayer: CALayer) { + + let imageProgress1 = NSImage(named: "articleExtractorProgress1") + let imageProgress2 = NSImage(named: "articleExtractorProgress2") + let imageProgress3 = NSImage(named: "articleExtractorProgress3") + let imageProgress4 = NSImage(named: "articleExtractorProgress4") + let images = [imageProgress1, imageProgress2, imageProgress3, imageProgress4, imageProgress3, imageProgress2] + + let imageLayer = CALayer() + imageLayer.bounds = CGRect(x: 0, y: 0, width: imageProgress1?.size.width ?? 0, height: imageProgress1?.size.height ?? 0) + imageLayer.position = CGPoint(x: bounds.midX, y: bounds.midY) + + hostedLayer.addSublayer(imageLayer) + + let animation = CAKeyframeAnimation(keyPath: "contents") + animation.calculationMode = CAAnimationCalculationMode.linear + animation.keyTimes = [0, 0.2, 0.4, 0.6, 0.8, 1] + animation.duration = 2 + animation.values = images as [Any] + animation.repeatCount = HUGE + + imageLayer.add(animation, forKey: "contents") + + } + +} diff --git a/Mac/MainWindow/Detail/DetailViewController.swift b/Mac/MainWindow/Detail/DetailViewController.swift index c1d26e512..592322b0d 100644 --- a/Mac/MainWindow/Detail/DetailViewController.swift +++ b/Mac/MainWindow/Detail/DetailViewController.swift @@ -16,6 +16,7 @@ enum DetailState: Equatable { case noSelection case multipleSelection case article(Article) + case extracted(Article, ExtractedArticle) } final class DetailViewController: NSViewController, WKUIDelegate { diff --git a/Mac/MainWindow/Detail/DetailWebViewController.swift b/Mac/MainWindow/Detail/DetailWebViewController.swift index 9e889db4b..4b9cd8f82 100644 --- a/Mac/MainWindow/Detail/DetailWebViewController.swift +++ b/Mac/MainWindow/Detail/DetailWebViewController.swift @@ -176,6 +176,8 @@ private extension DetailWebViewController { html = ArticleRenderer.multipleSelectionHTML(style: style) case .article(let article): html = ArticleRenderer.articleHTML(article: article, style: style) + case .extracted(let article, let extractedArticle): + html = ArticleRenderer.articleHTML(article: article, extractedArticle: extractedArticle, style: style) } webView.loadHTMLString(html, baseURL: nil) diff --git a/Mac/MainWindow/MainWindowController.swift b/Mac/MainWindow/MainWindowController.swift index bc00c98d1..566106e0c 100644 --- a/Mac/MainWindow/MainWindowController.swift +++ b/Mac/MainWindow/MainWindowController.swift @@ -17,6 +17,9 @@ enum TimelineSourceMode { class MainWindowController : NSWindowController, NSUserInterfaceValidations { + @IBOutlet weak var articleExtractorButton: ArticleExtractorButton! + + private var articleExtractor: ArticleExtractor? = nil private var sharingServicePickerDelegate: NSSharingServicePickerDelegate? private let windowAutosaveName = NSWindow.FrameAutosaveName("MainWindow") @@ -206,6 +209,10 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations { return canMarkOlderArticlesAsRead() } + if item.action == #selector(toggleArticleExtractor(_:)) { + return validateToggleArticleExtractor(item) + } + if item.action == #selector(toolbarShowShareMenu(_:)) { return canShowShareMenu() } @@ -292,6 +299,34 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations { currentTimelineViewController?.toggleStarredStatusForSelectedArticles() } + @IBAction func toggleArticleExtractor(_ sender: Any?) { + + guard let currentLink = currentLink, let article = oneSelectedArticle else { + return + } + + guard articleExtractorButton.state == .on else { + let detailState = DetailState.article(article) + detailViewController?.setState(detailState, mode: timelineSourceMode) + return + } + + if let articleExtractor = articleExtractor, let extractedArticle = articleExtractor.article { + if currentLink == articleExtractor.articleLink { + let detailState = DetailState.extracted(article, extractedArticle) + detailViewController?.setState(detailState, mode: timelineSourceMode) + } + } else { + if let extractor = ArticleExtractor(currentLink) { + extractor.delegate = self + extractor.process() + articleExtractor = extractor + makeToolbarValidate() + } + } + + } + @IBAction func markAllAsReadAndGoToNextUnread(_ sender: Any?) { markAllAsRead(sender) nextUnread(sender) @@ -407,6 +442,11 @@ extension MainWindowController: SidebarDelegate { extension MainWindowController: TimelineContainerViewControllerDelegate { func timelineSelectionDidChange(_: TimelineContainerViewController, articles: [Article]?, mode: TimelineSourceMode) { + articleExtractorButton.isError = false + articleExtractorButton.isInProgress = false + articleExtractorButton.state = .off + articleExtractor = nil + let detailState: DetailState if let articles = articles { detailState = articles.count == 1 ? .article(articles.first!) : .multipleSelection @@ -414,6 +454,7 @@ extension MainWindowController: TimelineContainerViewControllerDelegate { else { detailState = .noSelection } + detailViewController?.setState(detailState, mode: mode) } } @@ -481,6 +522,24 @@ extension MainWindowController: NSSearchFieldDelegate { } } +// MARK: - ArticleExtractorDelegate + +extension MainWindowController: ArticleExtractorDelegate { + + func articleExtractionDidFail(with: Error) { + makeToolbarValidate() + } + + func articleExtractionDidComplete(extractedArticle: ExtractedArticle) { + makeToolbarValidate() + if articleExtractorButton.state == .on, let article = oneSelectedArticle { + let detailState = DetailState.extracted(article, extractedArticle) + detailViewController?.setState(detailState, mode: timelineSourceMode) + } + } + +} + // MARK: - Scripting Access /* @@ -632,6 +691,34 @@ private extension MainWindowController { return result } + func validateToggleArticleExtractor(_ item: NSValidatedUserInterfaceItem) -> Bool { + guard let articleExtractorState = articleExtractor?.state else { + articleExtractorButton.isError = false + articleExtractorButton.isInProgress = false + return currentLink != nil + } + + switch articleExtractorState { + case .ready: + articleExtractorButton.isError = false + articleExtractorButton.isInProgress = false + return currentLink != nil + case .processing: + articleExtractorButton.isError = false + articleExtractorButton.isInProgress = true + return true + case .failedToParse: + articleExtractorButton.isError = true + articleExtractorButton.isInProgress = false + articleExtractorButton.state = .off + return true + case .complete: + articleExtractorButton.isError = false + articleExtractorButton.isInProgress = false + return currentLink != nil + } + } + func canMarkOlderArticlesAsRead() -> Bool { return currentTimelineViewController?.canMarkOlderArticlesAsRead() ?? false diff --git a/Mac/Resources/Assets.xcassets/articleExtractor.imageset/Contents.json b/Mac/Resources/Assets.xcassets/articleExtractor.imageset/Contents.json new file mode 100644 index 000000000..20aecf925 --- /dev/null +++ b/Mac/Resources/Assets.xcassets/articleExtractor.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "FullArticle.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Mac/Resources/Assets.xcassets/articleExtractor.imageset/FullArticle.pdf b/Mac/Resources/Assets.xcassets/articleExtractor.imageset/FullArticle.pdf new file mode 100644 index 0000000000000000000000000000000000000000..af687ae42d973a7fb42c54d5f32281a9b8f2e236 GIT binary patch literal 4081 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 literal 0 HcmV?d00001 diff --git a/Mac/Resources/Assets.xcassets/articleExtractorError.imageset/Contents.json b/Mac/Resources/Assets.xcassets/articleExtractorError.imageset/Contents.json new file mode 100644 index 000000000..100f19ee7 --- /dev/null +++ b/Mac/Resources/Assets.xcassets/articleExtractorError.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "FullArticleError.png" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Mac/Resources/Assets.xcassets/articleExtractorError.imageset/FullArticleError.png b/Mac/Resources/Assets.xcassets/articleExtractorError.imageset/FullArticleError.png new file mode 100644 index 0000000000000000000000000000000000000000..25785ee6b99ae2747d6186044401bfed585e9be4 GIT binary patch literal 282 zcmV+#0pPx#)Ja4^R5%f}Qri*2Fbqp_Sp%l&KCl7p2Cz@3DT9RASVkm)9v|ecXIYkCNkgy` zr`!D!N|`}~C*y7f({(HA3xEU5!U5Lu#tTkwy=owGq4j)JA662RWd|XF*`^TVEFbq$ zf$=O>%q_d%i0~mX80<*=x-W`m^yDqTi7x9Fl%Y^Iu;cdtqP3Z>cJVg$wC*E|dxPtr zzDCy5VG#imP^r7^s@a`U3Vl1ggX++2n8d&BP|%RF0@i3k?)pPHQ^BVx_KI;0#s8h4 g{w;W|uJbR%10z#*x)x%r)Bpeg07*qoM6N<$g5Ak_qW}N^ literal 0 HcmV?d00001 diff --git a/Mac/Resources/Assets.xcassets/articleExtractorProgress1.imageset/Contents.json b/Mac/Resources/Assets.xcassets/articleExtractorProgress1.imageset/Contents.json new file mode 100644 index 000000000..866dc9c65 --- /dev/null +++ b/Mac/Resources/Assets.xcassets/articleExtractorProgress1.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "FullArticleProgress1.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Mac/Resources/Assets.xcassets/articleExtractorProgress1.imageset/FullArticleProgress1.pdf b/Mac/Resources/Assets.xcassets/articleExtractorProgress1.imageset/FullArticleProgress1.pdf new file mode 100644 index 0000000000000000000000000000000000000000..1a9e77b3dab0d266957b923d6c987a478d4c0bc3 GIT binary patch literal 4105 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 literal 0 HcmV?d00001 diff --git a/Mac/Resources/Assets.xcassets/articleExtractorProgress3.imageset/Contents.json b/Mac/Resources/Assets.xcassets/articleExtractorProgress3.imageset/Contents.json new file mode 100644 index 000000000..67c09f9f7 --- /dev/null +++ b/Mac/Resources/Assets.xcassets/articleExtractorProgress3.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "FullArticleProgress3.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Mac/Resources/Assets.xcassets/articleExtractorProgress3.imageset/FullArticleProgress3.pdf b/Mac/Resources/Assets.xcassets/articleExtractorProgress3.imageset/FullArticleProgress3.pdf new file mode 100644 index 0000000000000000000000000000000000000000..816f830711e9dbd2dceb404fd59ecce2dfad5ae2 GIT binary patch literal 4102 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 literal 0 HcmV?d00001 diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 922cfbdd6..9caac5e83 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -199,6 +199,13 @@ 51F85BF92274AA7B00C787DC /* UIBarButtonItem-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F85BF82274AA7B00C787DC /* UIBarButtonItem-Extensions.swift */; }; 51F85BFB2275D85000C787DC /* Array-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F85BFA2275D85000C787DC /* Array-Extensions.swift */; }; 51F85BFD2275DCA800C787DC /* SingleLineUILabelSizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F85BFC2275DCA800C787DC /* SingleLineUILabelSizer.swift */; }; + 51FA73A42332BE110090D516 /* ArticleExtractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FA73A32332BE110090D516 /* ArticleExtractor.swift */; }; + 51FA73A52332BE110090D516 /* ArticleExtractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FA73A32332BE110090D516 /* ArticleExtractor.swift */; }; + 51FA73A72332BE880090D516 /* ExtractedArticle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FA73A62332BE880090D516 /* ExtractedArticle.swift */; }; + 51FA73A82332BE880090D516 /* ExtractedArticle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FA73A62332BE880090D516 /* ExtractedArticle.swift */; }; + 51FA73AA2332C2FD0090D516 /* ArticleExtractorConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FA73A92332C2FD0090D516 /* ArticleExtractorConfig.swift */; }; + 51FA73AB2332C2FD0090D516 /* ArticleExtractorConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FA73A92332C2FD0090D516 /* ArticleExtractorConfig.swift */; }; + 51FA73B72332D5F70090D516 /* ArticleExtractorButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FA73B62332D5F70090D516 /* ArticleExtractorButton.swift */; }; 55E15BCB229D65A900D6602A /* AccountsReaderAPI.xib in Resources */ = {isa = PBXBuildFile; fileRef = 55E15BC1229D65A900D6602A /* AccountsReaderAPI.xib */; }; 55E15BCC229D65A900D6602A /* AccountsReaderAPIWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55E15BCA229D65A900D6602A /* AccountsReaderAPIWindowController.swift */; }; 5F323809231DF9F000706F6B /* NNWTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F323808231DF9F000706F6B /* NNWTableViewCell.swift */; }; @@ -863,6 +870,10 @@ 51F85BF82274AA7B00C787DC /* UIBarButtonItem-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIBarButtonItem-Extensions.swift"; sourceTree = ""; }; 51F85BFA2275D85000C787DC /* Array-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array-Extensions.swift"; sourceTree = ""; }; 51F85BFC2275DCA800C787DC /* SingleLineUILabelSizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SingleLineUILabelSizer.swift; sourceTree = ""; }; + 51FA73A32332BE110090D516 /* ArticleExtractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleExtractor.swift; sourceTree = ""; }; + 51FA73A62332BE880090D516 /* ExtractedArticle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtractedArticle.swift; sourceTree = ""; }; + 51FA73A92332C2FD0090D516 /* ArticleExtractorConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleExtractorConfig.swift; sourceTree = ""; }; + 51FA73B62332D5F70090D516 /* ArticleExtractorButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleExtractorButton.swift; sourceTree = ""; }; 557EE1A522B6F4E1004206FA /* SettingsReaderAPIAccountView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsReaderAPIAccountView.swift; sourceTree = ""; }; 55E15BC1229D65A900D6602A /* AccountsReaderAPI.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = AccountsReaderAPI.xib; sourceTree = ""; }; 55E15BCA229D65A900D6602A /* AccountsReaderAPIWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountsReaderAPIWindowController.swift; sourceTree = ""; }; @@ -1391,6 +1402,16 @@ name = Frameworks; sourceTree = ""; }; + 51FA739A2332BDE70090D516 /* Article Extractor */ = { + isa = PBXGroup; + children = ( + 51FA73A92332C2FD0090D516 /* ArticleExtractorConfig.swift */, + 51FA73A32332BE110090D516 /* ArticleExtractor.swift */, + 51FA73A62332BE880090D516 /* ExtractedArticle.swift */, + ); + path = "Article Extractor"; + sourceTree = ""; + }; 6581C73620CED60100F4AD34 /* SafariExtension */ = { isa = PBXGroup; children = ( @@ -1456,6 +1477,7 @@ 849A975D1ED9EB72007D329B /* MainWindowController.swift */, 519B8D322143397200FA689C /* SharingServiceDelegate.swift */, 849EE72020391F560082A1EA /* SharingServicePickerDelegate.swift */, + 51FA73B62332D5F70090D516 /* ArticleExtractorButton.swift */, 844B5B6B1FEA224B00C7C76A /* Keyboard */, 849A975F1ED9EB95007D329B /* Sidebar */, 849A97681ED9EBC8007D329B /* Timeline */, @@ -1807,6 +1829,7 @@ 51C452AD2265102800C03939 /* Timeline */, 84702AB31FA27AE8006B8943 /* Commands */, 51934CCC231078DC006127BE /* Activity */, + 51FA739A2332BDE70090D516 /* Article Extractor */, 51C452A822650DA100C03939 /* Article Rendering */, 849A97861ED9ECEF007D329B /* Article Styles */, 84DAEE201F86CAE00058304B /* Importers */, @@ -2645,10 +2668,12 @@ 51322859232FDDB80033D4ED /* VibrantButtonStyle.swift in Sources */, 514B7C8323205EFB00BAC947 /* RootSplitViewController.swift in Sources */, 5152E0F923248F6200E5C7AD /* SettingsLocalAccountView.swift in Sources */, + 51FA73A52332BE110090D516 /* ArticleExtractor.swift in Sources */, FF3ABF162325AF5D0074C542 /* ArticleSorter.swift in Sources */, 510BD15D232D765D002692E4 /* SettingsReaderAPIAccountView.swift in Sources */, 51C4525C226508DF00C03939 /* String-Extensions.swift in Sources */, 51C452792265091600C03939 /* MasterTimelineTableViewCell.swift in Sources */, + 51FA73AB2332C2FD0090D516 /* ArticleExtractorConfig.swift in Sources */, 5132285B232FF2C40033D4ED /* SettingsRefreshSelectionView.swift in Sources */, 51C452852265093600C03939 /* FlattenedAccountFolderPickerData.swift in Sources */, 51C4526B226508F600C03939 /* MasterFeedViewController.swift in Sources */, @@ -2676,6 +2701,7 @@ 5183CCE5226F4DFA0010922C /* RefreshInterval.swift in Sources */, 51C4529D22650A1000C03939 /* FaviconURLFinder.swift in Sources */, 51C45258226508CF00C03939 /* AppAssets.swift in Sources */, + 51FA73A82332BE880090D516 /* ExtractedArticle.swift in Sources */, 51C4527C2265091600C03939 /* MasterTimelineDefaultCellLayout.swift in Sources */, 51C4529A22650A0400C03939 /* ArticleStyle.swift in Sources */, 51C4527F2265092C00C03939 /* DetailViewController.swift in Sources */, @@ -2736,6 +2762,7 @@ 84F204E01FAACBB30076E152 /* ArticleArray.swift in Sources */, 848B937221C8C5540038DC0D /* CrashReporter.swift in Sources */, 847CD6CA232F4CBF00FAC46D /* TimelineAvatarView.swift in Sources */, + 51FA73AA2332C2FD0090D516 /* ArticleExtractorConfig.swift in Sources */, 84BBB12E20142A4700F054F5 /* InspectorWindowController.swift in Sources */, 51EF0F7A22771B890050506E /* ColorHash.swift in Sources */, 84E46C7D1F75EF7B005ECFB3 /* AppDefaults.swift in Sources */, @@ -2815,6 +2842,7 @@ 841ABA5E20145E9200980E11 /* FolderInspectorViewController.swift in Sources */, 84DEE56522C32CA4005FC42C /* SmartFeedDelegate.swift in Sources */, 845213231FCA5B11003B6E93 /* ImageDownloader.swift in Sources */, + 51FA73B72332D5F70090D516 /* ArticleExtractorButton.swift in Sources */, 51EF0F922279CA620050506E /* AccountsAddTableCellView.swift in Sources */, 849A97431ED9EAA9007D329B /* AddFolderWindowController.swift in Sources */, 8405DDA522168C62008CE1BF /* TimelineContainerViewController.swift in Sources */, @@ -2838,6 +2866,7 @@ 84E8E0EB202F693600562D8F /* DetailWebView.swift in Sources */, 849A976C1ED9EBC8007D329B /* TimelineTableRowView.swift in Sources */, 849A977B1ED9EC04007D329B /* UnreadIndicatorView.swift in Sources */, + 51FA73A72332BE880090D516 /* ExtractedArticle.swift in Sources */, 84B99C9D1FAE83C600ECDEDB /* DeleteCommand.swift in Sources */, 849A97541ED9EAC0007D329B /* AddFeedWindowController.swift in Sources */, 5144EA40227A37EC00D19003 /* ImportOPMLWindowController.swift in Sources */, @@ -2846,6 +2875,7 @@ D5E4CC64202C1AC1009B4FFC /* MainWindowController+Scriptability.swift in Sources */, 84C9FC7922629E1200D921D6 /* PreferencesWindowController.swift in Sources */, 84411E711FE5FBFA004B527F /* SmallIconProvider.swift in Sources */, + 51FA73A42332BE110090D516 /* ArticleExtractor.swift in Sources */, 84CAFCA422BC8C08007694F0 /* FetchRequestQueue.swift in Sources */, 844B5B591FE9FE4F00C7C76A /* SidebarKeyboardDelegate.swift in Sources */, 84C9FC7C22629E1200D921D6 /* AccountsPreferencesViewController.swift in Sources */, diff --git a/NetNewsWire.xcodeproj/xcshareddata/xcschemes/NetNewsWire.xcscheme b/NetNewsWire.xcodeproj/xcshareddata/xcschemes/NetNewsWire.xcscheme index 230b6714a..20a782e7f 100644 --- a/NetNewsWire.xcodeproj/xcshareddata/xcschemes/NetNewsWire.xcscheme +++ b/NetNewsWire.xcodeproj/xcshareddata/xcschemes/NetNewsWire.xcscheme @@ -58,9 +58,6 @@ useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" - stopOnEveryThreadSanitizerIssue = "YES" - stopOnEveryUBSanitizerIssue = "YES" - stopOnEveryMainThreadCheckerIssue = "YES" migratedStopOnEveryIssue = "YES" debugServiceExtension = "internal" allowLocationSimulation = "YES"> @@ -74,6 +71,18 @@ ReferencedContainer = "container:NetNewsWire.xcodeproj"> + + + + + + String? { + let processInfo = ProcessInfo.processInfo + guard let value = processInfo.environment[named] else { + print("‼️ Missing Environment Variable: '\(named)'") + return nil + } + return value + } + +} diff --git a/Shared/Article Extractor/ExtractedArticle.swift b/Shared/Article Extractor/ExtractedArticle.swift new file mode 100644 index 000000000..460302477 --- /dev/null +++ b/Shared/Article Extractor/ExtractedArticle.swift @@ -0,0 +1,45 @@ +// +// ExtractedArticle.swift +// NetNewsWire +// +// Created by Maurice Parker on 9/18/19. +// Copyright © 2019 Ranchero Software. All rights reserved. +// + +import Foundation + +struct ExtractedArticle: Codable, Equatable { + + let title: String? + let author: String? + let datePublished: String? + let dek: String? + let leadImageURL: String? + let content: String? + let nextPageURL: String? + let url: String? + let domain: String? + let excerpt: String? + let wordCount: Int? + let direction: String? + let totalPages: Int? + let renderedPages: Int? + + enum CodingKeys: String, CodingKey { + case title = "title" + case author = "author" + case datePublished = "date_published" + case dek = "dek" + case leadImageURL = "lead_image_url" + case content = "content" + case nextPageURL = "next_page_url" + case url = "url" + case domain = "domain" + case excerpt = "excerpt" + case wordCount = "word_count" + case direction = "direction" + case totalPages = "total_pages" + case renderedPages = "rendered_pages" + } + +} diff --git a/Shared/Article Rendering/ArticleRenderer.swift b/Shared/Article Rendering/ArticleRenderer.swift index 49554ac25..5359bb414 100644 --- a/Shared/Article Rendering/ArticleRenderer.swift +++ b/Shared/Article Rendering/ArticleRenderer.swift @@ -14,36 +14,44 @@ import Account struct ArticleRenderer { private let article: Article? + private let extractedArticle: ExtractedArticle? private let articleStyle: ArticleStyle private let title: String + private let body: String private let baseURL: String? - private init(article: Article?, style: ArticleStyle) { + private init(article: Article?, extractedArticle: ExtractedArticle?, style: ArticleStyle) { self.article = article + self.extractedArticle = extractedArticle self.articleStyle = style self.title = article?.title ?? "" + if let content = extractedArticle?.content { + self.body = content + } else { + self.body = article?.body ?? "" + } self.baseURL = article?.baseURL?.absoluteString } // MARK: - API - static func articleHTML(article: Article, style: ArticleStyle) -> String { - let renderer = ArticleRenderer(article: article, style: style) + static func articleHTML(article: Article, extractedArticle: ExtractedArticle? = nil, style: ArticleStyle) -> String { + let renderer = ArticleRenderer(article: article, extractedArticle: extractedArticle, style: style) return renderer.articleHTML } static func multipleSelectionHTML(style: ArticleStyle) -> String { - let renderer = ArticleRenderer(article: nil, style: style) + let renderer = ArticleRenderer(article: nil, extractedArticle: nil, style: style) return renderer.multipleSelectionHTML } static func noSelectionHTML(style: ArticleStyle) -> String { - let renderer = ArticleRenderer(article: nil, style: style) + let renderer = ArticleRenderer(article: nil, extractedArticle: nil, style: style) return renderer.noSelectionHTML } static func noContentHTML(style: ArticleStyle) -> String { - let renderer = ArticleRenderer(article: nil, style: style) + let renderer = ArticleRenderer(article: nil, extractedArticle: nil, style: style) return renderer.noContentHTML } } @@ -53,7 +61,7 @@ struct ArticleRenderer { private extension ArticleRenderer { private var articleHTML: String { - let body = RSMacroProcessor.renderedText(withTemplate: template(), substitutions: substitutions(), macroStart: "[[", macroEnd: "]]") + let body = RSMacroProcessor.renderedText(withTemplate: template(), substitutions: articleSubstitutions(), macroStart: "[[", macroEnd: "]]") return renderHTML(withBody: body) } @@ -101,7 +109,7 @@ private extension ArticleRenderer { return title } - func substitutions() -> [String: String] { + func articleSubstitutions() -> [String: String] { var d = [String: String]() guard let article = article else { @@ -112,7 +120,6 @@ private extension ArticleRenderer { let title = titleOrTitleLink() d["title"] = title - let body = article.body ?? "" d["body"] = body d["avatars"] = "" diff --git a/submodules/RSCore b/submodules/RSCore index 98c050aca..4dbd31b09 160000 --- a/submodules/RSCore +++ b/submodules/RSCore @@ -1 +1 @@ -Subproject commit 98c050aca6e2c4f034b22c1d3d4f938893290543 +Subproject commit 4dbd31b090ab15c3966e9810a65edbf4abdbdd33