From 2708a608c5fa5db0c62abc1ce339f15b28087582 Mon Sep 17 00:00:00 2001 From: Nate Weaver Date: Mon, 6 Apr 2020 15:22:01 -0500 Subject: [PATCH 01/19] Compile frameworks for 10.15+ --- Frameworks/Articles/xcconfig/Articles_project.xcconfig | 2 +- .../xcconfig/ArticlesDatabase_project.xcconfig | 2 +- Frameworks/SyncDatabase/xcconfig/SyncDatabase_project.xcconfig | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Frameworks/Articles/xcconfig/Articles_project.xcconfig b/Frameworks/Articles/xcconfig/Articles_project.xcconfig index 573284972..d3bd6c6de 100644 --- a/Frameworks/Articles/xcconfig/Articles_project.xcconfig +++ b/Frameworks/Articles/xcconfig/Articles_project.xcconfig @@ -9,7 +9,7 @@ PROVISIONING_PROFILE_SPECIFIER = #include? "../../../SharedXcodeSettings/DeveloperSettings.xcconfig" SDKROOT = macosx -MACOSX_DEPLOYMENT_TARGET = 10.14 +MACOSX_DEPLOYMENT_TARGET = 10.15 IPHONEOS_DEPLOYMENT_TARGET = 13.0 SUPPORTED_PLATFORMS = macosx iphoneos iphonesimulator diff --git a/Frameworks/ArticlesDatabase/xcconfig/ArticlesDatabase_project.xcconfig b/Frameworks/ArticlesDatabase/xcconfig/ArticlesDatabase_project.xcconfig index 573284972..d3bd6c6de 100644 --- a/Frameworks/ArticlesDatabase/xcconfig/ArticlesDatabase_project.xcconfig +++ b/Frameworks/ArticlesDatabase/xcconfig/ArticlesDatabase_project.xcconfig @@ -9,7 +9,7 @@ PROVISIONING_PROFILE_SPECIFIER = #include? "../../../SharedXcodeSettings/DeveloperSettings.xcconfig" SDKROOT = macosx -MACOSX_DEPLOYMENT_TARGET = 10.14 +MACOSX_DEPLOYMENT_TARGET = 10.15 IPHONEOS_DEPLOYMENT_TARGET = 13.0 SUPPORTED_PLATFORMS = macosx iphoneos iphonesimulator diff --git a/Frameworks/SyncDatabase/xcconfig/SyncDatabase_project.xcconfig b/Frameworks/SyncDatabase/xcconfig/SyncDatabase_project.xcconfig index 09f243ba8..d3bd6c6de 100644 --- a/Frameworks/SyncDatabase/xcconfig/SyncDatabase_project.xcconfig +++ b/Frameworks/SyncDatabase/xcconfig/SyncDatabase_project.xcconfig @@ -9,7 +9,7 @@ PROVISIONING_PROFILE_SPECIFIER = #include? "../../../SharedXcodeSettings/DeveloperSettings.xcconfig" SDKROOT = macosx -MACOSX_DEPLOYMENT_TARGET = 10.14 +MACOSX_DEPLOYMENT_TARGET = 10.15 IPHONEOS_DEPLOYMENT_TARGET = 13.0 SUPPORTED_PLATFORMS = macosx iphoneos iphonesimulator @@ -18,7 +18,6 @@ SWIFT_VERSION = 5.1 COMBINE_HIDPI_IMAGES = YES COPY_PHASE_STRIP = NO -MACOSX_DEPLOYMENT_TARGET = 10.14 ALWAYS_SEARCH_USER_PATHS = NO CURRENT_PROJECT_VERSION = 1 VERSION_INFO_PREFIX = From 4ddb6c8d4f29729898638d74e54a64d4c388e43b Mon Sep 17 00:00:00 2001 From: Nate Weaver Date: Tue, 7 Apr 2020 16:04:50 -0500 Subject: [PATCH 02/19] Only allow certain inline tags to remain in the title; HTML-encode the rest --- Frameworks/Articles/Article.swift | 35 +++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/Frameworks/Articles/Article.swift b/Frameworks/Articles/Article.swift index 6e13633cc..c97f94731 100644 --- a/Frameworks/Articles/Article.swift +++ b/Frameworks/Articles/Article.swift @@ -86,3 +86,38 @@ public extension Array where Element == Article { return map { $0.articleID } } } + +public extension Article { + static let allowedTags: Set = ["b", "bdi", "bdo", "cite", "code", "del", "dfn", "em", "i", "ins", "kbd", "mark", "q", "rb", "rp", "rt", "rtc", "ruby", "s", "samp", "small", "strong", "sub", "sup", "time", "u", "var"] + + func sanitizedTitle(forHTML: Bool = true) -> String? { + guard let title = title else { return nil } + + let scanner = Scanner(string: title) + scanner.charactersToBeSkipped = nil + var result = "" + result.reserveCapacity(title.count) + + while !scanner.isAtEnd { + if let text = scanner.scanUpToString("<") { + result.append(text) + } + + if let _ = scanner.scanString("<") { + // All the allowed tags currently don't allow attributes + if let tag = scanner.scanUpToString(">") { + if Self.allowedTags.contains(tag.replacingOccurrences(of: "/", with: "")) { + forHTML ? result.append("<\(tag)>") : result.append("") + } else { + forHTML ? result.append("<\(tag)>") : result.append("<\(tag)>") + } + + let _ = scanner.scanString(">") + } + } + } + + return result + } + +} From 3d509a94d4cdcb531f26943da5f61127ad8bcca9 Mon Sep 17 00:00:00 2001 From: Nate Weaver Date: Tue, 7 Apr 2020 16:05:13 -0500 Subject: [PATCH 03/19] Add attributed title support in the timeline --- .../Cell/MultilineTextFieldSizer.swift | 47 +++++++++++++ .../Cell/NSAttributedString+NetNewsWire.swift | 68 +++++++++++++++++++ .../Timeline/Cell/TimelineCellData.swift | 3 + .../Timeline/Cell/TimelineCellLayout.swift | 3 +- .../Timeline/Cell/TimelineTableCellView.swift | 14 ++++ NetNewsWire.xcodeproj/project.pbxproj | 6 ++ .../Article Rendering/ArticleRenderer.swift | 2 +- .../Extensions/ArticleStringFormatter.swift | 17 ++++- 8 files changed, 155 insertions(+), 5 deletions(-) create mode 100644 Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift diff --git a/Mac/MainWindow/Timeline/Cell/MultilineTextFieldSizer.swift b/Mac/MainWindow/Timeline/Cell/MultilineTextFieldSizer.swift index d5b979d25..de75fc46e 100644 --- a/Mac/MainWindow/Timeline/Cell/MultilineTextFieldSizer.swift +++ b/Mac/MainWindow/Timeline/Cell/MultilineTextFieldSizer.swift @@ -34,6 +34,7 @@ final class MultilineTextFieldSizer { private let singleLineHeightEstimate: Int private let doubleLineHeightEstimate: Int private var cache = [String: WidthHeightCache]() // Each string has a cache. + private var attributedCache = [NSAttributedString: WidthHeightCache]() private static var sizers = [TextFieldSizerSpecifier: MultilineTextFieldSizer]() private init(numberOfLines: Int, font: NSFont) { @@ -51,6 +52,14 @@ final class MultilineTextFieldSizer { return sizer(numberOfLines: numberOfLines, font: font).sizeInfo(for: string, width: width) } + static func size(for attributedString: NSAttributedString, numberOfLines: Int, width: Int) -> TextFieldSizeInfo { + + // Assumes the same font family/size for the whole string + let font = attributedString.attribute(.font, at: 0, effectiveRange: nil) as! NSFont + + return sizer(numberOfLines: numberOfLines, font: font).sizeInfo(for: attributedString, width: width) + } + static func emptyCache() { sizers = [TextFieldSizerSpecifier: MultilineTextFieldSizer]() @@ -83,6 +92,16 @@ private extension MultilineTextFieldSizer { return sizeInfo } + func sizeInfo(for attributedString: NSAttributedString, width: Int) -> TextFieldSizeInfo { + + let textFieldHeight = height(for: attributedString, width: width) + let numberOfLinesUsed = numberOfLines(for: textFieldHeight) + + let size = NSSize(width: width, height: textFieldHeight) + let sizeInfo = TextFieldSizeInfo(size: size, numberOfLinesUsed: numberOfLinesUsed) + return sizeInfo + } + func height(for string: String, width: Int) -> Int { if cache[string] == nil { @@ -103,6 +122,26 @@ private extension MultilineTextFieldSizer { return height } + func height(for attribtuedString: NSAttributedString, width: Int) -> Int { + + if attributedCache[attribtuedString] == nil { + attributedCache[attribtuedString] = WidthHeightCache() + } + + if let height = attributedCache[attribtuedString]![width] { + return height + } + + if let height = heightConsideringNeighbors(attributedCache[attribtuedString]!, width) { + return height + } + + let height = calculateHeight(attribtuedString, width) + attributedCache[attribtuedString]![width] = height + + return height + } + static func createTextField(_ numberOfLines: Int, _ font: NSFont) -> NSTextField { let textField = NSTextField(wrappingLabelWithString: "") @@ -120,6 +159,14 @@ private extension MultilineTextFieldSizer { return MultilineTextFieldSizer.calculateHeight(string, width, textField) } + func calculateHeight(_ attributedString: NSAttributedString, _ width: Int) -> Int { + + textField.attributedStringValue = attributedString + textField.preferredMaxLayoutWidth = CGFloat(width) + let size = textField.fittingSize + return Int(ceil(size.height)) + } + static func calculateHeight(_ string: String, _ width: Int, _ textField: NSTextField) -> Int { textField.stringValue = string diff --git a/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift b/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift new file mode 100644 index 000000000..60869c98c --- /dev/null +++ b/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift @@ -0,0 +1,68 @@ +// +// NSAttributedString+NetNewsWire.swift +// NetNewsWire +// +// Created by Nate Weaver on 2020-04-07. +// Copyright © 2020 Ranchero Software. All rights reserved. +// + +import AppKit + +extension NSAttributedString { + + func adding(font baseFont: NSFont, color: NSColor? = nil) -> NSAttributedString { + let mutable = self.mutableCopy() as! NSMutableAttributedString + let fullRange = NSRange(location: 0, length: mutable.length) + + if let color = color { + mutable.addAttribute(.foregroundColor, value: color as Any, range: fullRange) + } + + let size = baseFont.pointSize + let baseDescriptor = baseFont.fontDescriptor + let traits = baseDescriptor.symbolicTraits + + mutable.enumerateAttribute(.font, in: fullRange, options: []) { (font: Any?, range: NSRange, stop: UnsafeMutablePointer) in + guard let font = font as? NSFont else { return } + + var newTraits = traits + + if font.fontDescriptor.symbolicTraits.contains(.italic) { + newTraits.insert(.italic) + } + + var descriptor = baseDescriptor.withSymbolicTraits(newTraits) + + if font.fontDescriptor.symbolicTraits.contains(.bold) { + // This currently assumes we're modifying the title field, which is + // already semibold. + let traits: [NSFontDescriptor.TraitKey: Any] = [.weight: NSFont.Weight.heavy] + let attributes: [NSFontDescriptor.AttributeName: Any] = [.traits: traits] + descriptor = descriptor.addingAttributes(attributes) + } + + let newFont = NSFont(descriptor: descriptor, size: size) + + mutable.addAttribute(.font, value: newFont as Any, range: range) + } + + // make sup/sub smaller + mutable.enumerateAttributes(in: fullRange, options: []) { (attributes: [Key : Any], range: NSRange, stop: UnsafeMutablePointer) in + guard let superscript = attributes[.superscript] as? Int else { + return + } + + if superscript != 0 { + let font = mutable.attribute(.font, at: range.location, effectiveRange: nil) as! NSFont + let size = font.pointSize * 0.6 + + let newFont = NSFont(descriptor: font.fontDescriptor, size: size) + mutable.addAttribute(.font, value: newFont as Any, range: range) + } + + } + + return mutable.copy() as! NSAttributedString + } + +} diff --git a/Mac/MainWindow/Timeline/Cell/TimelineCellData.swift b/Mac/MainWindow/Timeline/Cell/TimelineCellData.swift index b6ca525bc..1e92a7714 100644 --- a/Mac/MainWindow/Timeline/Cell/TimelineCellData.swift +++ b/Mac/MainWindow/Timeline/Cell/TimelineCellData.swift @@ -12,6 +12,7 @@ import Articles struct TimelineCellData { let title: String + let attributedTitle: NSAttributedString let text: String let dateString: String let feedName: String @@ -26,6 +27,7 @@ struct TimelineCellData { init(article: Article, showFeedName: TimelineShowFeedName, feedName: String?, byline: String?, iconImage: IconImage?, showIcon: Bool, featuredImage: NSImage?) { self.title = ArticleStringFormatter.truncatedTitle(article) + self.attributedTitle = ArticleStringFormatter.attributedTruncatedTitle(article) self.text = ArticleStringFormatter.truncatedSummary(article) self.dateString = ArticleStringFormatter.dateString(article.logicalDatePublished) @@ -64,5 +66,6 @@ struct TimelineCellData { self.featuredImage = nil self.read = true self.starred = false + self.attributedTitle = NSAttributedString() } } diff --git a/Mac/MainWindow/Timeline/Cell/TimelineCellLayout.swift b/Mac/MainWindow/Timeline/Cell/TimelineCellLayout.swift index 07bb08c9b..df4fb42b2 100644 --- a/Mac/MainWindow/Timeline/Cell/TimelineCellLayout.swift +++ b/Mac/MainWindow/Timeline/Cell/TimelineCellLayout.swift @@ -115,7 +115,8 @@ private extension TimelineCellLayout { return (r, 0) } - let sizeInfo = MultilineTextFieldSizer.size(for: cellData.title, font: appearance.titleFont, numberOfLines: appearance.titleNumberOfLines, width: Int(textBoxRect.width)) + let attributedTitle = cellData.attributedTitle.adding(font: appearance.titleFont) + let sizeInfo = MultilineTextFieldSizer.size(for: attributedTitle, numberOfLines: appearance.titleNumberOfLines, width: Int(textBoxRect.width)) r.size.height = sizeInfo.size.height if sizeInfo.numberOfLinesUsed < 1 { r.size.height = 0 diff --git a/Mac/MainWindow/Timeline/Cell/TimelineTableCellView.swift b/Mac/MainWindow/Timeline/Cell/TimelineTableCellView.swift index eb1e8a79e..3151a7d3c 100644 --- a/Mac/MainWindow/Timeline/Cell/TimelineTableCellView.swift +++ b/Mac/MainWindow/Timeline/Cell/TimelineTableCellView.swift @@ -222,6 +222,7 @@ private extension TimelineTableCellView { func updateTitleView() { updateTextFieldText(titleView, cellData?.title) + updateTextFieldAttributedText(titleView, cellData?.attributedTitle) } func updateSummaryView() { @@ -247,6 +248,19 @@ private extension TimelineTableCellView { } } + func updateTextFieldAttributedText(_ textField: NSTextField, _ text: NSAttributedString?) { + var s = text ?? NSAttributedString(string: "") + + if let fieldFont = textField.font, let color = textField.textColor { + s = s.adding(font: fieldFont, color: color) + } + + if textField.attributedStringValue != s { + textField.attributedStringValue = s + needsLayout = true + } + } + func updateFeedNameView() { switch cellData.showFeedName { case .byline: diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 75846af92..4f38ea23b 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -725,6 +725,8 @@ 84FF69B11FC3793300DC198E /* FaviconURLFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FF69B01FC3793300DC198E /* FaviconURLFinder.swift */; }; B27EEBF9244D15F3000932E6 /* shared.css in Resources */ = {isa = PBXBuildFile; fileRef = B27EEBDF244D15F2000932E6 /* shared.css */; }; B27EEBFA244D15F3000932E6 /* shared.css in Resources */ = {isa = PBXBuildFile; fileRef = B27EEBDF244D15F2000932E6 /* shared.css */; }; + B26B9572243D176B0053EEF5 /* NSAttributedString+NetNewsWire.swift in Sources */ = {isa = PBXBuildFile; fileRef = B26B9571243D176B0053EEF5 /* NSAttributedString+NetNewsWire.swift */; }; + B26B9573243D176B0053EEF5 /* NSAttributedString+NetNewsWire.swift in Sources */ = {isa = PBXBuildFile; fileRef = B26B9571243D176B0053EEF5 /* NSAttributedString+NetNewsWire.swift */; }; B27EEBFB244D15F3000932E6 /* shared.css in Resources */ = {isa = PBXBuildFile; fileRef = B27EEBDF244D15F2000932E6 /* shared.css */; }; B2B8075E239C49D300F191E0 /* RSImage-AppIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B8075D239C49D300F191E0 /* RSImage-AppIcons.swift */; }; B2B80778239C4C7000F191E0 /* RSImage-AppIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B8075D239C49D300F191E0 /* RSImage-AppIcons.swift */; }; @@ -1784,6 +1786,7 @@ B24EFD482330FF99006C6242 /* NetNewsWire-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NetNewsWire-Bridging-Header.h"; sourceTree = ""; }; B24EFD5923310109006C6242 /* WKPreferencesPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WKPreferencesPrivate.h; sourceTree = ""; }; B27EEBDF244D15F2000932E6 /* shared.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = shared.css; sourceTree = ""; }; + B26B9571243D176B0053EEF5 /* NSAttributedString+NetNewsWire.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+NetNewsWire.swift"; sourceTree = ""; }; B2B8075D239C49D300F191E0 /* RSImage-AppIcons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RSImage-AppIcons.swift"; sourceTree = ""; }; B528F81D23333C7E00E735DD /* page.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = page.html; sourceTree = ""; }; BDCB514D24282C8A00102A80 /* AccountsNewsBlur.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = AccountsNewsBlur.xib; sourceTree = ""; }; @@ -2626,6 +2629,7 @@ 84E185C2203BB12600F69BFA /* MultilineTextFieldSizer.swift */, 849A97711ED9EC04007D329B /* TimelineCellData.swift */, 849A97751ED9EC04007D329B /* UnreadIndicatorView.swift */, + B26B9571243D176B0053EEF5 /* NSAttributedString+NetNewsWire.swift */, ); path = Cell; sourceTree = ""; @@ -4206,6 +4210,7 @@ 65ED4023235DEF6C0081F399 /* Folder+Scriptability.swift in Sources */, 65ED4024235DEF6C0081F399 /* TimelineCellLayout.swift in Sources */, 65ED4025235DEF6C0081F399 /* DetailWebView.swift in Sources */, + B26B9573243D176B0053EEF5 /* NSAttributedString+NetNewsWire.swift in Sources */, B2B80779239C4C7300F191E0 /* RSImage-AppIcons.swift in Sources */, 65ED4026235DEF6C0081F399 /* TimelineTableRowView.swift in Sources */, 65ED4027235DEF6C0081F399 /* UnreadIndicatorView.swift in Sources */, @@ -4541,6 +4546,7 @@ 8426119E1FCB6ED40086A189 /* HTMLMetadataDownloader.swift in Sources */, 849A976E1ED9EBC8007D329B /* TimelineViewController.swift in Sources */, 3B826DCC2385C84800FC1ADB /* AccountsFeedWranglerWindowController.swift in Sources */, + B26B9572243D176B0053EEF5 /* NSAttributedString+NetNewsWire.swift in Sources */, 5154368B229404D1005E1CDF /* FaviconGenerator.swift in Sources */, 5183CCE6226F4E110010922C /* RefreshInterval.swift in Sources */, 849A97771ED9EC04007D329B /* TimelineCellData.swift in Sources */, diff --git a/Shared/Article Rendering/ArticleRenderer.swift b/Shared/Article Rendering/ArticleRenderer.swift index ef7a25a52..3588615bd 100644 --- a/Shared/Article Rendering/ArticleRenderer.swift +++ b/Shared/Article Rendering/ArticleRenderer.swift @@ -46,7 +46,7 @@ struct ArticleRenderer { self.article = article self.extractedArticle = extractedArticle self.articleStyle = style - self.title = article?.title ?? "" + self.title = article?.sanitizedTitle() ?? "" if let content = extractedArticle?.content { self.body = content self.baseURL = extractedArticle?.url diff --git a/Shared/Extensions/ArticleStringFormatter.swift b/Shared/Extensions/ArticleStringFormatter.swift index 4fb9249ae..1ca644232 100644 --- a/Shared/Extensions/ArticleStringFormatter.swift +++ b/Shared/Extensions/ArticleStringFormatter.swift @@ -52,8 +52,8 @@ struct ArticleStringFormatter { return s } - static func truncatedTitle(_ article: Article) -> String { - guard let title = article.title else { + static func truncatedTitle(_ article: Article, forHTML: Bool = false) -> String { + guard let title = article.sanitizedTitle(forHTML: forHTML) else { return "" } @@ -64,7 +64,11 @@ struct ArticleStringFormatter { var s = title.replacingOccurrences(of: "\n", with: "") s = s.replacingOccurrences(of: "\r", with: "") s = s.replacingOccurrences(of: "\t", with: "") - s = s.rsparser_stringByDecodingHTMLEntities() + + if !forHTML { + s = s.rsparser_stringByDecodingHTMLEntities() + } + s = s.trimmingWhitespace s = s.collapsingWhitespace @@ -79,6 +83,13 @@ struct ArticleStringFormatter { return s } + static func attributedTruncatedTitle(_ article: Article) -> NSAttributedString { + let title = truncatedTitle(article, forHTML: true) + let data = title.data(using: .utf8)! + let attributed = NSAttributedString(html: data, documentAttributes: nil)! + return attributed + } + static func truncatedSummary(_ article: Article) -> String { guard let body = article.body else { return "" From 06b3d2f8e13afb1601da651e0ee05f1df9b4e596 Mon Sep 17 00:00:00 2001 From: Nate Weaver Date: Tue, 7 Apr 2020 16:20:47 -0500 Subject: [PATCH 04/19] Declare UTF-8 to avoid mojibake --- Shared/Extensions/ArticleStringFormatter.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Shared/Extensions/ArticleStringFormatter.swift b/Shared/Extensions/ArticleStringFormatter.swift index 1ca644232..731db43f2 100644 --- a/Shared/Extensions/ArticleStringFormatter.swift +++ b/Shared/Extensions/ArticleStringFormatter.swift @@ -86,7 +86,7 @@ struct ArticleStringFormatter { static func attributedTruncatedTitle(_ article: Article) -> NSAttributedString { let title = truncatedTitle(article, forHTML: true) let data = title.data(using: .utf8)! - let attributed = NSAttributedString(html: data, documentAttributes: nil)! + let attributed = NSAttributedString(html: data, options: [.characterEncoding: String.Encoding.utf8.rawValue], documentAttributes: nil)! return attributed } From 57a6561733a8bb4202fe151e3ff61c1e02927a48 Mon Sep 17 00:00:00 2001 From: Nate Weaver Date: Wed, 8 Apr 2020 06:50:54 -0500 Subject: [PATCH 05/19] Use superscript/subscript font feature --- .../Timeline/Cell/NSAttributedString+NetNewsWire.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift b/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift index 60869c98c..6be8a0501 100644 --- a/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift +++ b/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift @@ -54,10 +54,14 @@ extension NSAttributedString { if superscript != 0 { let font = mutable.attribute(.font, at: range.location, effectiveRange: nil) as! NSFont - let size = font.pointSize * 0.6 - let newFont = NSFont(descriptor: font.fontDescriptor, size: size) + let features: [NSFontDescriptor.FeatureKey: Any] = [.typeIdentifier: kVerticalPositionType, .selectorIdentifier: superscript > 0 ? kSuperiorsSelector : kInferiorsSelector] + let attributes: [NSFontDescriptor.AttributeName: Any] = [.featureSettings: [features]] + let descriptor = font.fontDescriptor.addingAttributes(attributes) + + let newFont = NSFont(descriptor: descriptor, size: font.pointSize) mutable.addAttribute(.font, value: newFont as Any, range: range) + mutable.addAttribute(.superscript, value: 0, range: range) } } From a6411c7afa0f8dd4e0242be894d01f8c005e8259 Mon Sep 17 00:00:00 2001 From: Nate Weaver Date: Wed, 8 Apr 2020 16:42:13 -0500 Subject: [PATCH 06/19] Check for semibold base font before changing bold text to heavy --- .../Cell/NSAttributedString+NetNewsWire.swift | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift b/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift index 6be8a0501..cfd60ce3c 100644 --- a/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift +++ b/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift @@ -20,25 +20,31 @@ extension NSAttributedString { let size = baseFont.pointSize let baseDescriptor = baseFont.fontDescriptor - let traits = baseDescriptor.symbolicTraits + let symbolicTraits = baseDescriptor.symbolicTraits mutable.enumerateAttribute(.font, in: fullRange, options: []) { (font: Any?, range: NSRange, stop: UnsafeMutablePointer) in guard let font = font as? NSFont else { return } - var newTraits = traits + var newSymbolicTraits = symbolicTraits if font.fontDescriptor.symbolicTraits.contains(.italic) { - newTraits.insert(.italic) + newSymbolicTraits.insert(.italic) } - var descriptor = baseDescriptor.withSymbolicTraits(newTraits) + var descriptor = baseDescriptor.withSymbolicTraits(newSymbolicTraits) if font.fontDescriptor.symbolicTraits.contains(.bold) { - // This currently assumes we're modifying the title field, which is - // already semibold. - let traits: [NSFontDescriptor.TraitKey: Any] = [.weight: NSFont.Weight.heavy] - let attributes: [NSFontDescriptor.AttributeName: Any] = [.traits: traits] - descriptor = descriptor.addingAttributes(attributes) + let baseTraits = baseDescriptor.object(forKey: .traits) as! [NSFontDescriptor.TraitKey: Any] + let baseWeight = baseTraits[.weight] as! NSFont.Weight + + // If the base font is semibold (as timeline titles are), make the "bold" + // text heavy for better contrast. + + if baseWeight == .semibold { + let traits: [NSFontDescriptor.TraitKey: Any] = [.weight: NSFont.Weight.heavy] + let attributes: [NSFontDescriptor.AttributeName: Any] = [.traits: traits] + descriptor = descriptor.addingAttributes(attributes) + } } let newFont = NSFont(descriptor: descriptor, size: size) From 39255a8422d43a4cd64d566857892364783ad9a2 Mon Sep 17 00:00:00 2001 From: Nate Weaver Date: Thu, 9 Apr 2020 14:01:26 -0500 Subject: [PATCH 07/19] Initialize baseWeight outside the enumeration --- .../Timeline/Cell/NSAttributedString+NetNewsWire.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift b/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift index cfd60ce3c..2fdc1f18e 100644 --- a/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift +++ b/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift @@ -22,6 +22,9 @@ extension NSAttributedString { let baseDescriptor = baseFont.fontDescriptor let symbolicTraits = baseDescriptor.symbolicTraits + let baseTraits = baseDescriptor.object(forKey: .traits) as! [NSFontDescriptor.TraitKey: Any] + let baseWeight = baseTraits[.weight] as! NSFont.Weight + mutable.enumerateAttribute(.font, in: fullRange, options: []) { (font: Any?, range: NSRange, stop: UnsafeMutablePointer) in guard let font = font as? NSFont else { return } @@ -34,9 +37,6 @@ extension NSAttributedString { var descriptor = baseDescriptor.withSymbolicTraits(newSymbolicTraits) if font.fontDescriptor.symbolicTraits.contains(.bold) { - let baseTraits = baseDescriptor.object(forKey: .traits) as! [NSFontDescriptor.TraitKey: Any] - let baseWeight = baseTraits[.weight] as! NSFont.Weight - // If the base font is semibold (as timeline titles are), make the "bold" // text heavy for better contrast. From db166ba0d7ad31d27b3c169f6aeac7b821e40b07 Mon Sep 17 00:00:00 2001 From: Nate Weaver Date: Fri, 10 Apr 2020 15:33:16 -0500 Subject: [PATCH 08/19] Disable ruby tags for now --- Frameworks/Articles/Article.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Frameworks/Articles/Article.swift b/Frameworks/Articles/Article.swift index c97f94731..d2fbdda77 100644 --- a/Frameworks/Articles/Article.swift +++ b/Frameworks/Articles/Article.swift @@ -88,7 +88,7 @@ public extension Array where Element == Article { } public extension Article { - static let allowedTags: Set = ["b", "bdi", "bdo", "cite", "code", "del", "dfn", "em", "i", "ins", "kbd", "mark", "q", "rb", "rp", "rt", "rtc", "ruby", "s", "samp", "small", "strong", "sub", "sup", "time", "u", "var"] + static let allowedTags: Set = ["b", "bdi", "bdo", "cite", "code", "del", "dfn", "em", "i", "ins", "kbd", "mark", "q", /* "rb", "rp", "rt", "rtc", "ruby", */ "s", "samp", "small", "strong", "sub", "sup", "time", "u", "var"] func sanitizedTitle(forHTML: Bool = true) -> String? { guard let title = title else { return nil } From 2a00b4a4a9a9b70c66d5c4af9f2b72d0ae23731c Mon Sep 17 00:00:00 2001 From: Nate Weaver Date: Fri, 10 Apr 2020 15:53:11 -0500 Subject: [PATCH 09/19] Keep monospace fonts monospaced; use a variable for the current symbolic traits --- .../Cell/NSAttributedString+NetNewsWire.swift | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift b/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift index 2fdc1f18e..6c57c2e83 100644 --- a/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift +++ b/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift @@ -20,7 +20,7 @@ extension NSAttributedString { let size = baseFont.pointSize let baseDescriptor = baseFont.fontDescriptor - let symbolicTraits = baseDescriptor.symbolicTraits + let baseSymbolicTraits = baseDescriptor.symbolicTraits let baseTraits = baseDescriptor.object(forKey: .traits) as! [NSFontDescriptor.TraitKey: Any] let baseWeight = baseTraits[.weight] as! NSFont.Weight @@ -28,15 +28,21 @@ extension NSAttributedString { mutable.enumerateAttribute(.font, in: fullRange, options: []) { (font: Any?, range: NSRange, stop: UnsafeMutablePointer) in guard let font = font as? NSFont else { return } - var newSymbolicTraits = symbolicTraits + var newSymbolicTraits = baseSymbolicTraits - if font.fontDescriptor.symbolicTraits.contains(.italic) { + let symbolicTraits = font.fontDescriptor.symbolicTraits + + if symbolicTraits.contains(.italic) { newSymbolicTraits.insert(.italic) } + if symbolicTraits.contains(.monoSpace) { + newSymbolicTraits.insert(.monoSpace) + } + var descriptor = baseDescriptor.withSymbolicTraits(newSymbolicTraits) - if font.fontDescriptor.symbolicTraits.contains(.bold) { + if symbolicTraits.contains(.bold) { // If the base font is semibold (as timeline titles are), make the "bold" // text heavy for better contrast. From d84ed6068f45fe9642a4a995444b061418495167 Mon Sep 17 00:00:00 2001 From: Nate Weaver Date: Fri, 10 Apr 2020 16:02:49 -0500 Subject: [PATCH 10/19] Add a convenience initializer for attributed strings from an html string --- .../Timeline/Cell/NSAttributedString+NetNewsWire.swift | 5 +++++ Shared/Extensions/ArticleStringFormatter.swift | 3 +-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift b/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift index 6c57c2e83..28a5d89df 100644 --- a/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift +++ b/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift @@ -81,4 +81,9 @@ extension NSAttributedString { return mutable.copy() as! NSAttributedString } + convenience init(html: String) { + let data = html.data(using: .utf8)! + self.init(html: data, options: [.characterEncoding: String.Encoding.utf8.rawValue], documentAttributes: nil)! + } + } diff --git a/Shared/Extensions/ArticleStringFormatter.swift b/Shared/Extensions/ArticleStringFormatter.swift index 731db43f2..dac926b34 100644 --- a/Shared/Extensions/ArticleStringFormatter.swift +++ b/Shared/Extensions/ArticleStringFormatter.swift @@ -85,8 +85,7 @@ struct ArticleStringFormatter { static func attributedTruncatedTitle(_ article: Article) -> NSAttributedString { let title = truncatedTitle(article, forHTML: true) - let data = title.data(using: .utf8)! - let attributed = NSAttributedString(html: data, options: [.characterEncoding: String.Encoding.utf8.rawValue], documentAttributes: nil)! + let attributed = NSAttributedString(html: title) return attributed } From ba73e3289a8f318a04b9448c623169f9ab6c8e90 Mon Sep 17 00:00:00 2001 From: Nate Weaver Date: Fri, 10 Apr 2020 16:07:32 -0500 Subject: [PATCH 11/19] Remove redundant call to updateTextFieldText() --- Mac/MainWindow/Timeline/Cell/TimelineTableCellView.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Mac/MainWindow/Timeline/Cell/TimelineTableCellView.swift b/Mac/MainWindow/Timeline/Cell/TimelineTableCellView.swift index 3151a7d3c..d0b9a7802 100644 --- a/Mac/MainWindow/Timeline/Cell/TimelineTableCellView.swift +++ b/Mac/MainWindow/Timeline/Cell/TimelineTableCellView.swift @@ -221,7 +221,6 @@ private extension TimelineTableCellView { func updateTitleView() { - updateTextFieldText(titleView, cellData?.title) updateTextFieldAttributedText(titleView, cellData?.attributedTitle) } From dc787620c5ca2845243ff3eb724be29794d131eb Mon Sep 17 00:00:00 2001 From: Nate Weaver Date: Wed, 15 Apr 2020 16:45:21 -0500 Subject: [PATCH 12/19] Use API that works on both macOS and iOS --- .../Timeline/Cell/NSAttributedString+NetNewsWire.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift b/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift index 28a5d89df..b572001ee 100644 --- a/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift +++ b/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift @@ -83,7 +83,8 @@ extension NSAttributedString { convenience init(html: String) { let data = html.data(using: .utf8)! - self.init(html: data, options: [.characterEncoding: String.Encoding.utf8.rawValue], documentAttributes: nil)! + let options: [NSAttributedString.DocumentReadingOptionKey: Any] = [.characterEncoding: String.Encoding.utf8.rawValue, .documentType: NSAttributedString.DocumentType.html] + try! self.init(data: data, options: options, documentAttributes: nil) } } From 6aff83481f277620e20f4e2e4a51f4367fe20d86 Mon Sep 17 00:00:00 2001 From: Nate Weaver Date: Wed, 15 Apr 2020 16:34:16 -0500 Subject: [PATCH 13/19] Make attributed titles work on iOS --- .../Cell/NSAttributedString+NetNewsWire.swift | 66 +++++++++++++------ NetNewsWire.xcodeproj/project.pbxproj | 2 + .../Cell/MasterTimelineCellData.swift | 3 + .../Cell/MasterTimelineTableViewCell.swift | 15 ++++- 4 files changed, 65 insertions(+), 21 deletions(-) diff --git a/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift b/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift index b572001ee..576701c17 100644 --- a/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift +++ b/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift @@ -6,11 +6,29 @@ // Copyright © 2020 Ranchero Software. All rights reserved. // +#if canImport(AppKit) import AppKit +typealias Font = NSFont +typealias FontDescriptor = NSFontDescriptor +typealias Color = NSColor + +private let boldTrait = NSFontDescriptor.SymbolicTraits.bold +private let italicTrait = NSFontDescriptor.SymbolicTraits.italic +private let monoSpaceTrait = NSFontDescriptor.SymbolicTraits.monoSpace +#else +import UIKit +typealias Font = UIFont +typealias FontDescriptor = UIFontDescriptor +typealias Color = UIColor + +private let boldTrait = UIFontDescriptor.SymbolicTraits.traitBold +private let italicTrait = UIFontDescriptor.SymbolicTraits.traitItalic +private let monoSpaceTrait = UIFontDescriptor.SymbolicTraits.traitMonoSpace +#endif extension NSAttributedString { - func adding(font baseFont: NSFont, color: NSColor? = nil) -> NSAttributedString { + func adding(font baseFont: Font, color: Color? = nil) -> NSAttributedString { let mutable = self.mutableCopy() as! NSMutableAttributedString let fullRange = NSRange(location: 0, length: mutable.length) @@ -22,60 +40,68 @@ extension NSAttributedString { let baseDescriptor = baseFont.fontDescriptor let baseSymbolicTraits = baseDescriptor.symbolicTraits - let baseTraits = baseDescriptor.object(forKey: .traits) as! [NSFontDescriptor.TraitKey: Any] - let baseWeight = baseTraits[.weight] as! NSFont.Weight + let baseTraits = baseDescriptor.object(forKey: .traits) as! [FontDescriptor.TraitKey: Any] + let baseWeight = baseTraits[.weight] as! Font.Weight mutable.enumerateAttribute(.font, in: fullRange, options: []) { (font: Any?, range: NSRange, stop: UnsafeMutablePointer) in - guard let font = font as? NSFont else { return } + guard let font = font as? Font else { return } var newSymbolicTraits = baseSymbolicTraits let symbolicTraits = font.fontDescriptor.symbolicTraits - if symbolicTraits.contains(.italic) { - newSymbolicTraits.insert(.italic) + if symbolicTraits.contains(italicTrait) { + newSymbolicTraits.insert(italicTrait) } - if symbolicTraits.contains(.monoSpace) { - newSymbolicTraits.insert(.monoSpace) + if symbolicTraits.contains(monoSpaceTrait) { + newSymbolicTraits.insert(monoSpaceTrait) } + #if canImport(AppKit) var descriptor = baseDescriptor.withSymbolicTraits(newSymbolicTraits) + #else + var descriptor = baseDescriptor.withSymbolicTraits(newSymbolicTraits)! + #endif - if symbolicTraits.contains(.bold) { + if symbolicTraits.contains(boldTrait) { // If the base font is semibold (as timeline titles are), make the "bold" // text heavy for better contrast. if baseWeight == .semibold { - let traits: [NSFontDescriptor.TraitKey: Any] = [.weight: NSFont.Weight.heavy] - let attributes: [NSFontDescriptor.AttributeName: Any] = [.traits: traits] + let traits: [FontDescriptor.TraitKey: Any] = [.weight: Font.Weight.heavy] + let attributes: [FontDescriptor.AttributeName: Any] = [.traits: traits] descriptor = descriptor.addingAttributes(attributes) } } - let newFont = NSFont(descriptor: descriptor, size: size) + let newFont = Font(descriptor: descriptor, size: size) mutable.addAttribute(.font, value: newFont as Any, range: range) } - // make sup/sub smaller + // make sup/sub smaller. `Key("NSSupeScript")` is used here because `.superscript` + // isn't defined in UIKit, for some reason. mutable.enumerateAttributes(in: fullRange, options: []) { (attributes: [Key : Any], range: NSRange, stop: UnsafeMutablePointer) in - guard let superscript = attributes[.superscript] as? Int else { + guard let superscript = attributes[Key("NSSuperScript")] as? Int else { return } if superscript != 0 { - let font = mutable.attribute(.font, at: range.location, effectiveRange: nil) as! NSFont + let font = mutable.attribute(.font, at: range.location, effectiveRange: nil) as! Font - let features: [NSFontDescriptor.FeatureKey: Any] = [.typeIdentifier: kVerticalPositionType, .selectorIdentifier: superscript > 0 ? kSuperiorsSelector : kInferiorsSelector] - let attributes: [NSFontDescriptor.AttributeName: Any] = [.featureSettings: [features]] + #if canImport(AppKit) + let features: [FontDescriptor.FeatureKey: Any] = [.typeIdentifier: kVerticalPositionType, .selectorIdentifier: superscript > 0 ? kSuperiorsSelector : kInferiorsSelector] + #else + let features: [FontDescriptor.FeatureKey: Any] = [.featureIdentifier: kVerticalPositionType, .typeIdentifier: superscript > 0 ? kSuperiorsSelector : kInferiorsSelector] + #endif + let attributes: [FontDescriptor.AttributeName: Any] = [.featureSettings: [features]] let descriptor = font.fontDescriptor.addingAttributes(attributes) - let newFont = NSFont(descriptor: descriptor, size: font.pointSize) + let newFont = Font(descriptor: descriptor, size: font.pointSize) mutable.addAttribute(.font, value: newFont as Any, range: range) - mutable.addAttribute(.superscript, value: 0, range: range) + mutable.addAttribute(Key("NSSuperScript"), value: 0, range: range) } - } return mutable.copy() as! NSAttributedString diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 4f38ea23b..da3069bc9 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -731,6 +731,7 @@ B2B8075E239C49D300F191E0 /* RSImage-AppIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B8075D239C49D300F191E0 /* RSImage-AppIcons.swift */; }; B2B80778239C4C7000F191E0 /* RSImage-AppIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B8075D239C49D300F191E0 /* RSImage-AppIcons.swift */; }; B2B80779239C4C7300F191E0 /* RSImage-AppIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B8075D239C49D300F191E0 /* RSImage-AppIcons.swift */; }; + B2C0FDEA2447A69100ADC150 /* NSAttributedString+NetNewsWire.swift in Sources */ = {isa = PBXBuildFile; fileRef = B26B9571243D176B0053EEF5 /* NSAttributedString+NetNewsWire.swift */; }; B528F81E23333C7E00E735DD /* page.html in Resources */ = {isa = PBXBuildFile; fileRef = B528F81D23333C7E00E735DD /* page.html */; }; BDCB516724282C8A00102A80 /* AccountsNewsBlur.xib in Resources */ = {isa = PBXBuildFile; fileRef = BDCB514D24282C8A00102A80 /* AccountsNewsBlur.xib */; }; BDCB516824282C8A00102A80 /* AccountsNewsBlur.xib in Resources */ = {isa = PBXBuildFile; fileRef = BDCB514D24282C8A00102A80 /* AccountsNewsBlur.xib */; }; @@ -4307,6 +4308,7 @@ 51F9F3F923DFB16300A314FD /* UITableView-Extensions.swift in Sources */, 51C452792265091600C03939 /* MasterTimelineTableViewCell.swift in Sources */, 51C4526B226508F600C03939 /* MasterFeedViewController.swift in Sources */, + B2C0FDEA2447A69100ADC150 /* NSAttributedString+NetNewsWire.swift in Sources */, 5126EE97226CB48A00C22AFC /* SceneCoordinator.swift in Sources */, 84CAFCB022BC8C35007694F0 /* FetchRequestOperation.swift in Sources */, 51EF0F77227716200050506E /* FaviconGenerator.swift in Sources */, diff --git a/iOS/MasterTimeline/Cell/MasterTimelineCellData.swift b/iOS/MasterTimeline/Cell/MasterTimelineCellData.swift index 8996654f8..d1d1c3782 100644 --- a/iOS/MasterTimeline/Cell/MasterTimelineCellData.swift +++ b/iOS/MasterTimeline/Cell/MasterTimelineCellData.swift @@ -12,6 +12,7 @@ import Articles struct MasterTimelineCellData { let title: String + let attributedTitle: NSAttributedString let summary: String let dateString: String let feedName: String @@ -28,6 +29,7 @@ struct MasterTimelineCellData { init(article: Article, showFeedName: ShowFeedName, feedName: String?, byline: String?, iconImage: IconImage?, showIcon: Bool, featuredImage: UIImage?, numberOfLines: Int, iconSize: IconSize) { self.title = ArticleStringFormatter.truncatedTitle(article) + self.attributedTitle = ArticleStringFormatter.attributedTruncatedTitle(article) self.summary = ArticleStringFormatter.truncatedSummary(article) self.dateString = ArticleStringFormatter.dateString(article.logicalDatePublished) @@ -60,6 +62,7 @@ struct MasterTimelineCellData { init() { //Empty self.title = "" + self.attributedTitle = NSAttributedString() self.summary = "" self.dateString = "" self.feedName = "" diff --git a/iOS/MasterTimeline/Cell/MasterTimelineTableViewCell.swift b/iOS/MasterTimeline/Cell/MasterTimelineTableViewCell.swift index 863ee7631..0c2d9d4a5 100644 --- a/iOS/MasterTimeline/Cell/MasterTimelineTableViewCell.swift +++ b/iOS/MasterTimeline/Cell/MasterTimelineTableViewCell.swift @@ -148,7 +148,7 @@ private extension MasterTimelineTableViewCell { func updateTitleView() { titleView.font = MasterTimelineDefaultCellLayout.titleFont titleView.textColor = labelColor - updateTextFieldText(titleView, cellData?.title) + updateTextFieldAttributedText(titleView, cellData?.attributedTitle) } func updateSummaryView() { @@ -170,6 +170,19 @@ private extension MasterTimelineTableViewCell { setNeedsLayout() } } + + func updateTextFieldAttributedText(_ label: UILabel, _ text: NSAttributedString?) { + var s = text ?? NSAttributedString(string: "") + + if let fieldFont = label.font, let color = label.textColor { + s = s.adding(font: fieldFont, color: color) + } + + if label.attributedText != s { + label.attributedText = s + setNeedsLayout() + } + } func updateFeedNameView() { switch cellData.showFeedName { From d3e6606cf5065966c660acf8208a0948390ba377 Mon Sep 17 00:00:00 2001 From: Nate Weaver Date: Wed, 15 Apr 2020 16:52:23 -0500 Subject: [PATCH 14/19] Use a constant for clarity --- .../Timeline/Cell/NSAttributedString+NetNewsWire.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift b/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift index 576701c17..fcca01f35 100644 --- a/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift +++ b/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift @@ -82,8 +82,10 @@ extension NSAttributedString { // make sup/sub smaller. `Key("NSSupeScript")` is used here because `.superscript` // isn't defined in UIKit, for some reason. + let superscriptAttribute = Key("NSSuperScript") + mutable.enumerateAttributes(in: fullRange, options: []) { (attributes: [Key : Any], range: NSRange, stop: UnsafeMutablePointer) in - guard let superscript = attributes[Key("NSSuperScript")] as? Int else { + guard let superscript = attributes[superscriptAttribute] as? Int else { return } @@ -100,7 +102,7 @@ extension NSAttributedString { let newFont = Font(descriptor: descriptor, size: font.pointSize) mutable.addAttribute(.font, value: newFont as Any, range: range) - mutable.addAttribute(Key("NSSuperScript"), value: 0, range: range) + mutable.addAttribute(superscriptAttribute, value: 0, range: range) } } From df4ea1479add447ef51699c80950f87f7163a3f5 Mon Sep 17 00:00:00 2001 From: Nate Weaver Date: Thu, 16 Apr 2020 14:55:39 -0500 Subject: [PATCH 15/19] Tweak comment --- .../Timeline/Cell/NSAttributedString+NetNewsWire.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift b/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift index fcca01f35..9fbdc5585 100644 --- a/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift +++ b/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift @@ -81,7 +81,7 @@ extension NSAttributedString { } // make sup/sub smaller. `Key("NSSupeScript")` is used here because `.superscript` - // isn't defined in UIKit, for some reason. + // isn't defined in UIKit. let superscriptAttribute = Key("NSSuperScript") mutable.enumerateAttributes(in: fullRange, options: []) { (attributes: [Key : Any], range: NSRange, stop: UnsafeMutablePointer) in From 26415f0c62616a389478404d84ba95f8f221c9ac Mon Sep 17 00:00:00 2001 From: Nate Weaver Date: Thu, 16 Apr 2020 15:38:27 -0500 Subject: [PATCH 16/19] Assign multiple attributes in one go rather than one at a time --- .../Timeline/Cell/NSAttributedString+NetNewsWire.swift | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift b/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift index 9fbdc5585..dcc2605d5 100644 --- a/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift +++ b/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift @@ -101,8 +101,13 @@ extension NSAttributedString { let descriptor = font.fontDescriptor.addingAttributes(attributes) let newFont = Font(descriptor: descriptor, size: font.pointSize) - mutable.addAttribute(.font, value: newFont as Any, range: range) - mutable.addAttribute(superscriptAttribute, value: 0, range: range) + + let newAttributes: [NSAttributedString.Key: Any] = [ + .font: newFont as Any, + superscriptAttribute: 0, + ] + + mutable.addAttributes(newAttributes, range: range) } } From 154e78b0c0dd90773489e13f66f7fdf647d4f015 Mon Sep 17 00:00:00 2001 From: Nate Weaver Date: Thu, 16 Apr 2020 15:40:14 -0500 Subject: [PATCH 17/19] Add a comment about why these constants differ between OSs --- .../Timeline/Cell/NSAttributedString+NetNewsWire.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift b/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift index dcc2605d5..bad680a91 100644 --- a/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift +++ b/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift @@ -92,6 +92,8 @@ extension NSAttributedString { if superscript != 0 { let font = mutable.attribute(.font, at: range.location, effectiveRange: nil) as! Font + // There's some discrepancy here: The raw value of AppKit's .typeIdentifier is UIKit's .featureIdentifier, + // and AppKit's .selectorIdentifier is UIKit's .typeIdentifier #if canImport(AppKit) let features: [FontDescriptor.FeatureKey: Any] = [.typeIdentifier: kVerticalPositionType, .selectorIdentifier: superscript > 0 ? kSuperiorsSelector : kInferiorsSelector] #else From fde4f53f426a65fa611023b449a8ad9d59664345 Mon Sep 17 00:00:00 2001 From: Nate Weaver Date: Thu, 16 Apr 2020 18:54:08 -0500 Subject: [PATCH 18/19] Documentation comments --- .../Timeline/Cell/NSAttributedString+NetNewsWire.swift | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift b/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift index bad680a91..afd4fada9 100644 --- a/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift +++ b/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift @@ -28,6 +28,14 @@ private let monoSpaceTrait = UIFontDescriptor.SymbolicTraits.traitMonoSpace extension NSAttributedString { + /// Adds a font and color to an attributed string. + /// + /// Additionally converts super-/subscript runs to super-/subscripted font variants + /// and converts bold text to heavy if the base font is semibold. + /// + /// - Parameters: + /// - baseFont: The font to add. + /// - color: The color to add. func adding(font baseFont: Font, color: Color? = nil) -> NSAttributedString { let mutable = self.mutableCopy() as! NSMutableAttributedString let fullRange = NSRange(location: 0, length: mutable.length) @@ -116,6 +124,7 @@ extension NSAttributedString { return mutable.copy() as! NSAttributedString } + /// Creates an attributed string from HTML. convenience init(html: String) { let data = html.data(using: .utf8)! let options: [NSAttributedString.DocumentReadingOptionKey: Any] = [.characterEncoding: String.Encoding.utf8.rawValue, .documentType: NSAttributedString.DocumentType.html] From b437ddab5c7665e28bbc1c218af4822fafcbe5ef Mon Sep 17 00:00:00 2001 From: Nate Weaver Date: Fri, 24 Apr 2020 18:25:56 -0500 Subject: [PATCH 19/19] Move NSAttributedString+NetNewsWire into Shared/Extensions --- NetNewsWire.xcodeproj/project.pbxproj | 39 ++++++++++++------- .../NSAttributedString+NetNewsWire.swift | 0 2 files changed, 26 insertions(+), 13 deletions(-) rename {Mac/MainWindow/Timeline/Cell => Shared/Extensions}/NSAttributedString+NetNewsWire.swift (100%) diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index da3069bc9..cda8dc64c 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -723,10 +723,10 @@ 84F9EAF4213660A100CF2DE4 /* testGenericScript.applescript in Sources */ = {isa = PBXBuildFile; fileRef = 84F9EAE1213660A100CF2DE4 /* testGenericScript.applescript */; }; 84F9EAF5213660A100CF2DE4 /* establishMainWindowStartingState.applescript in Sources */ = {isa = PBXBuildFile; fileRef = 84F9EAE2213660A100CF2DE4 /* establishMainWindowStartingState.applescript */; }; 84FF69B11FC3793300DC198E /* FaviconURLFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FF69B01FC3793300DC198E /* FaviconURLFinder.swift */; }; - B27EEBF9244D15F3000932E6 /* shared.css in Resources */ = {isa = PBXBuildFile; fileRef = B27EEBDF244D15F2000932E6 /* shared.css */; }; - B27EEBFA244D15F3000932E6 /* shared.css in Resources */ = {isa = PBXBuildFile; fileRef = B27EEBDF244D15F2000932E6 /* shared.css */; }; B26B9572243D176B0053EEF5 /* NSAttributedString+NetNewsWire.swift in Sources */ = {isa = PBXBuildFile; fileRef = B26B9571243D176B0053EEF5 /* NSAttributedString+NetNewsWire.swift */; }; B26B9573243D176B0053EEF5 /* NSAttributedString+NetNewsWire.swift in Sources */ = {isa = PBXBuildFile; fileRef = B26B9571243D176B0053EEF5 /* NSAttributedString+NetNewsWire.swift */; }; + B27EEBF9244D15F3000932E6 /* shared.css in Resources */ = {isa = PBXBuildFile; fileRef = B27EEBDF244D15F2000932E6 /* shared.css */; }; + B27EEBFA244D15F3000932E6 /* shared.css in Resources */ = {isa = PBXBuildFile; fileRef = B27EEBDF244D15F2000932E6 /* shared.css */; }; B27EEBFB244D15F3000932E6 /* shared.css in Resources */ = {isa = PBXBuildFile; fileRef = B27EEBDF244D15F2000932E6 /* shared.css */; }; B2B8075E239C49D300F191E0 /* RSImage-AppIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B8075D239C49D300F191E0 /* RSImage-AppIcons.swift */; }; B2B80778239C4C7000F191E0 /* RSImage-AppIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B8075D239C49D300F191E0 /* RSImage-AppIcons.swift */; }; @@ -1260,6 +1260,13 @@ remoteGlobalIDString = 84F22C541B52E0D9000060CE; remoteInfo = RSDatabase; }; + B234554A2453AB0F000F1D7F /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 517A754424451BD500B553B9 /* OAuthSwift.xcodeproj */; + proxyType = 1; + remoteGlobalIDString = C48B28011AFA598D00C7DEF6; + remoteInfo = OAuthSwiftMacOS; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -1786,8 +1793,8 @@ 84FF69B01FC3793300DC198E /* FaviconURLFinder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconURLFinder.swift; sourceTree = ""; }; B24EFD482330FF99006C6242 /* NetNewsWire-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NetNewsWire-Bridging-Header.h"; sourceTree = ""; }; B24EFD5923310109006C6242 /* WKPreferencesPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WKPreferencesPrivate.h; sourceTree = ""; }; - B27EEBDF244D15F2000932E6 /* shared.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = shared.css; sourceTree = ""; }; B26B9571243D176B0053EEF5 /* NSAttributedString+NetNewsWire.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+NetNewsWire.swift"; sourceTree = ""; }; + B27EEBDF244D15F2000932E6 /* shared.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = shared.css; sourceTree = ""; }; B2B8075D239C49D300F191E0 /* RSImage-AppIcons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RSImage-AppIcons.swift"; sourceTree = ""; }; B528F81D23333C7E00E735DD /* page.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = page.html; sourceTree = ""; }; BDCB514D24282C8A00102A80 /* AccountsNewsBlur.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = AccountsNewsBlur.xib; sourceTree = ""; }; @@ -2581,6 +2588,7 @@ B2B8075D239C49D300F191E0 /* RSImage-AppIcons.swift */, 51126DA3225FDE2F00722696 /* RSImage-Extensions.swift */, 84411E701FE5FBFA004B527F /* SmallIconProvider.swift */, + B26B9571243D176B0053EEF5 /* NSAttributedString+NetNewsWire.swift */, ); path = Extensions; sourceTree = ""; @@ -2630,7 +2638,6 @@ 84E185C2203BB12600F69BFA /* MultilineTextFieldSizer.swift */, 849A97711ED9EC04007D329B /* TimelineCellData.swift */, 849A97751ED9EC04007D329B /* UnreadIndicatorView.swift */, - B26B9571243D176B0053EEF5 /* NSAttributedString+NetNewsWire.swift */, ); path = Cell; sourceTree = ""; @@ -3252,6 +3259,7 @@ 84C37FB020DD8D9900CA8CF5 /* PBXTargetDependency */, 84C37FB820DD8DBB00CA8CF5 /* PBXTargetDependency */, 84C37FC820DD8E1D00CA8CF5 /* PBXTargetDependency */, + B234554B2453AB0F000F1D7F /* PBXTargetDependency */, 51C451AC226377C300C03939 /* PBXTargetDependency */, 51C451BC226377C900C03939 /* PBXTargetDependency */, 51C451C0226377D000C03939 /* PBXTargetDependency */, @@ -3298,36 +3306,36 @@ TargetAttributes = { 51314636235A7BBE00387FDC = { CreatedOnToolsVersion = 11.2; - DevelopmentTeam = SHJK2V3AJG; + DevelopmentTeam = M72QZ9W58G; LastSwiftMigration = 1120; ProvisioningStyle = Automatic; }; 513C5CE5232571C2003D4054 = { CreatedOnToolsVersion = 11.0; - DevelopmentTeam = SHJK2V3AJG; + DevelopmentTeam = M72QZ9W58G; ProvisioningStyle = Automatic; }; 518B2ED12351B3DD00400001 = { CreatedOnToolsVersion = 11.2; - DevelopmentTeam = SHJK2V3AJG; + DevelopmentTeam = M72QZ9W58G; ProvisioningStyle = Automatic; TestTargetID = 840D617B2029031C009BC708; }; 6581C73220CED60000F4AD34 = { - DevelopmentTeam = SHJK2V3AJG; + DevelopmentTeam = M72QZ9W58G; ProvisioningStyle = Automatic; }; 65ED3FA2235DEF6C0081F399 = { - DevelopmentTeam = SHJK2V3AJG; + DevelopmentTeam = M72QZ9W58G; ProvisioningStyle = Automatic; }; 65ED4090235DEF770081F399 = { - DevelopmentTeam = SHJK2V3AJG; + DevelopmentTeam = M72QZ9W58G; ProvisioningStyle = Automatic; }; 840D617B2029031C009BC708 = { CreatedOnToolsVersion = 9.3; - DevelopmentTeam = SHJK2V3AJG; + DevelopmentTeam = M72QZ9W58G; ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.BackgroundModes = { @@ -3337,7 +3345,7 @@ }; 849C645F1ED37A5D003D8FC0 = { CreatedOnToolsVersion = 8.2.1; - DevelopmentTeam = SHJK2V3AJG; + DevelopmentTeam = M72QZ9W58G; ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.HardenedRuntime = { @@ -3347,7 +3355,7 @@ }; 849C64701ED37A5D003D8FC0 = { CreatedOnToolsVersion = 8.2.1; - DevelopmentTeam = SHJK2V3AJG; + DevelopmentTeam = M72QZ9W58G; ProvisioningStyle = Automatic; TestTargetID = 849C645F1ED37A5D003D8FC0; }; @@ -4763,6 +4771,11 @@ name = RSDatabase; targetProxy = 84C37FC720DD8E1D00CA8CF5 /* PBXContainerItemProxy */; }; + B234554B2453AB0F000F1D7F /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = OAuthSwiftMacOS; + targetProxy = B234554A2453AB0F000F1D7F /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ diff --git a/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift b/Shared/Extensions/NSAttributedString+NetNewsWire.swift similarity index 100% rename from Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift rename to Shared/Extensions/NSAttributedString+NetNewsWire.swift