Merge pull request #2035 from Wevah/title-tags

Updated title tags implementation
This commit is contained in:
Maurice Parker 2020-05-01 17:43:23 -05:00 committed by GitHub
commit 7696fced21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 442 additions and 15 deletions

View File

@ -92,3 +92,38 @@ public extension Array where Element == Article {
return map { $0.articleID }
}
}
public extension Article {
private static let allowedTags: Set = ["b", "bdi", "bdo", "cite", "code", "del", "dfn", "em", "i", "ins", "kbd", "mark", "q", "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("&lt;\(tag)&gt;") : result.append("<\(tag)>")
}
let _ = scanner.scanString(">")
}
}
}
return result
}
}

View File

@ -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

View File

@ -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

View File

@ -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 =

View File

@ -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

View File

@ -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()
}
}

View File

@ -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

View File

@ -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 {
s = s.adding(font: fieldFont)
}
if textField.attributedStringValue != s {
textField.attributedStringValue = s
needsLayout = true
}
}
func updateFeedNameView() {
switch cellData.showFeedName {
case .byline:

View File

@ -723,6 +723,9 @@
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 */; };
B24E9ADC245AB88400DA5718 /* NSAttributedString+NetNewsWire.swift in Sources */ = {isa = PBXBuildFile; fileRef = B24E9ABA245AB88300DA5718 /* NSAttributedString+NetNewsWire.swift */; };
B24E9ADD245AB88400DA5718 /* NSAttributedString+NetNewsWire.swift in Sources */ = {isa = PBXBuildFile; fileRef = B24E9ABA245AB88300DA5718 /* NSAttributedString+NetNewsWire.swift */; };
B24E9ADE245AB88400DA5718 /* NSAttributedString+NetNewsWire.swift in Sources */ = {isa = PBXBuildFile; fileRef = B24E9ABA245AB88300DA5718 /* 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 */; };
@ -1780,6 +1783,7 @@
84F9EAE2213660A100CF2DE4 /* establishMainWindowStartingState.applescript */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.applescript; path = establishMainWindowStartingState.applescript; sourceTree = "<group>"; };
84F9EAE4213660A100CF2DE4 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
84FF69B01FC3793300DC198E /* FaviconURLFinder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconURLFinder.swift; sourceTree = "<group>"; };
B24E9ABA245AB88300DA5718 /* NSAttributedString+NetNewsWire.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+NetNewsWire.swift"; sourceTree = "<group>"; };
B24EFD482330FF99006C6242 /* NetNewsWire-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NetNewsWire-Bridging-Header.h"; sourceTree = "<group>"; };
B24EFD5923310109006C6242 /* WKPreferencesPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WKPreferencesPrivate.h; sourceTree = "<group>"; };
B27EEBDF244D15F2000932E6 /* shared.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = shared.css; sourceTree = "<group>"; };
@ -2566,6 +2570,7 @@
849A97561ED9EB0D007D329B /* Extensions */ = {
isa = PBXGroup;
children = (
B24E9ABA245AB88300DA5718 /* NSAttributedString+NetNewsWire.swift */,
51A66684238075AE00CB272D /* AddWebFeedDefaultContainer.swift */,
849A97731ED9EC04007D329B /* ArticleStringFormatter.swift */,
849A97581ED9EB0D007D329B /* ArticleUtilities.swift */,
@ -4129,6 +4134,7 @@
65ED3FDD235DEF6C0081F399 /* ActivityType.swift in Sources */,
65ED3FDE235DEF6C0081F399 /* CrashReportWindowController.swift in Sources */,
65ED3FDF235DEF6C0081F399 /* WebFeedIconDownloader.swift in Sources */,
B24E9ADD245AB88400DA5718 /* NSAttributedString+NetNewsWire.swift in Sources */,
65ED3FE0235DEF6C0081F399 /* PreferencesControlsBackgroundView.swift in Sources */,
65ED3FE1235DEF6C0081F399 /* MarkCommandValidationStatus.swift in Sources */,
65ED3FE2235DEF6C0081F399 /* ArticlePasteboardWriter.swift in Sources */,
@ -4385,6 +4391,7 @@
514219372352510100E07E2C /* ImageScrollView.swift in Sources */,
516AE9B32371C372007DEEAA /* MasterFeedTableViewSectionHeaderLayout.swift in Sources */,
51DC370B2405BC9A0095D371 /* PreloadedWebView.swift in Sources */,
B24E9ADE245AB88400DA5718 /* NSAttributedString+NetNewsWire.swift in Sources */,
C5A6ED5223C9AF4300AB6BE2 /* TitleActivityItemSource.swift in Sources */,
51DC37092402F1470095D371 /* MasterFeedDataSourceOperation.swift in Sources */,
51C4529B22650A1000C03939 /* FaviconDownloader.swift in Sources */,
@ -4494,6 +4501,7 @@
FF3ABF1523259DDB0074C542 /* ArticleSorter.swift in Sources */,
84E8E0DB202EC49300562D8F /* TimelineViewController+ContextualMenus.swift in Sources */,
849A97791ED9EC04007D329B /* ArticleStringFormatter.swift in Sources */,
B24E9ADC245AB88400DA5718 /* NSAttributedString+NetNewsWire.swift in Sources */,
84E185C3203BB12600F69BFA /* MultilineTextFieldSizer.swift in Sources */,
8477ACBE22238E9500DF7F37 /* SearchFeedDelegate.swift in Sources */,
51E3EB33229AB02C00645299 /* ErrorHandler.swift in Sources */,

View File

@ -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

View File

@ -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,12 @@ struct ArticleStringFormatter {
return s
}
static func attributedTruncatedTitle(_ article: Article) -> NSAttributedString {
let title = truncatedTitle(article, forHTML: true)
let attributed = NSAttributedString(html: title)
return attributed
}
static func truncatedSummary(_ article: Article) -> String {
guard let body = article.body else {
return ""

View File

@ -0,0 +1,294 @@
//
// NSAttributedString+NetNewsWire.swift
// NetNewsWire
//
// Created by Nate Weaver on 2020-04-07.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import RSParser
#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 {
/// Adds a font and color to an attributed string.
///
/// - Parameters:
/// - baseFont: The font to add.
func adding(font baseFont: Font) -> NSAttributedString {
let mutable = self.mutableCopy() as! NSMutableAttributedString
let fullRange = NSRange(location: 0, length: mutable.length)
let size = baseFont.pointSize
let baseDescriptor = baseFont.fontDescriptor
let baseSymbolicTraits = baseDescriptor.symbolicTraits
mutable.enumerateAttribute(.font, in: fullRange, options: []) { (font: Any?, range: NSRange, stop: UnsafeMutablePointer<ObjCBool>) in
guard let font = font as? Font else { return }
let currentDescriptor = font.fontDescriptor
let symbolicTraits = baseSymbolicTraits.union(currentDescriptor.symbolicTraits)
var descriptor = currentDescriptor.addingAttributes(baseDescriptor.fontAttributes)
#if canImport(AppKit)
descriptor = descriptor.withSymbolicTraits(symbolicTraits)
#else
descriptor = descriptor.withSymbolicTraits(symbolicTraits)!
#endif
let newFont = Font(descriptor: descriptor, size: size)
mutable.addAttribute(.font, value: newFont as Any, range: range)
}
return mutable.copy() as! NSAttributedString
}
private enum InTag {
case none
case opening
case closing
}
private enum Style {
case bold
case italic
case superscript
case `subscript`
case underline
case strikethrough
case monospace
init?(forTag: String) {
switch forTag {
case "b", "strong":
self = .bold
case "i", "em", "cite", "var", "dfn":
self = .italic
case "sup":
self = .superscript
case "sub":
self = .subscript
case "u", "ins":
self = .underline
case "s", "del":
self = .strikethrough
case "code", "samp", "tt", "kbd":
self = .monospace
default:
return nil
}
}
}
/// Returns an attributed string initialized from HTML text containing basic inline stylistic tags.
///
/// - Parameters:
/// - html: The HTML text.
/// - locale: The locale used for quotation marks when parsing `<q>` tags.
convenience init(html: String, locale: Locale = Locale.current) {
let baseFont = Font.systemFont(ofSize: Font.systemFontSize)
var inTag: InTag = .none
var tag = ""
var currentStyles = CountedSet<Style>()
var iterator = html.makeIterator()
let result = NSMutableAttributedString()
var attributeRanges = [ (range: NSRange, styles: CountedSet<Style>) ]()
var quoteDepth = 0
while let char = iterator.next() {
if char == "<" && inTag == .none {
tag.removeAll()
guard let first = iterator.next() else { break }
if first == "/" {
inTag = .closing
} else {
inTag = .opening
tag.append(first)
}
} else if char == ">" && inTag != .none {
let lastRange = attributeRanges.last?.range
let location = lastRange != nil ? lastRange!.location + lastRange!.length : 0
let range = NSRange(location: location, length: result.mutableString.length - location)
attributeRanges.append( (range: range, styles: currentStyles) )
if inTag == .opening {
if tag == "q" {
quoteDepth += 1
let delimiter = quoteDepth % 2 == 1 ? locale.quotationBeginDelimiter : locale.alternateQuotationBeginDelimiter
result.mutableString.append(delimiter ?? "\"")
}
if let style = Style(forTag: tag) {
currentStyles.insert(style)
}
} else {
if tag == "q" {
let delimiter = quoteDepth % 2 == 1 ? locale.quotationEndDelimiter : locale.alternateQuotationEndDelimiter
result.mutableString.append(delimiter ?? "\"")
quoteDepth -= 1
}
if let style = Style(forTag: tag) {
currentStyles.remove(style)
}
}
inTag = .none
} else if inTag != .none {
tag.append(char)
} else {
if char == "&" {
var entity = "&"
var lastchar: Character? = nil
while let entitychar = iterator.next() {
if entitychar.isWhitespace {
lastchar = entitychar
break;
}
entity.append(entitychar)
if (entitychar == ";") { break }
}
result.mutableString.append(entity.decodedEntity)
if let lastchar = lastchar { result.mutableString.append(String(lastchar)) }
} else {
result.mutableString.append(String(char))
}
}
}
result.addAttribute(.font, value: baseFont, range: NSRange(location: 0, length: result.length))
for (range, styles) in attributeRanges {
let currentFont = result.attribute(.font, at: range.location, effectiveRange: nil) as! Font
let currentDescriptor = currentFont.fontDescriptor
var descriptor = currentDescriptor.copy() as! FontDescriptor
var symbolicTraits = currentDescriptor.symbolicTraits
if styles.contains(.bold) {
let traits: [FontDescriptor.TraitKey: Any] = [.weight: Font.Weight.bold]
let attributes: [FontDescriptor.AttributeName: Any] = [.traits: traits]
descriptor = descriptor.addingAttributes(attributes)
symbolicTraits.insert(boldTrait)
}
if styles.contains(.italic) {
symbolicTraits.insert(italicTrait)
}
if styles.contains(.monospace) {
symbolicTraits.insert(monoSpaceTrait)
}
#if canImport(AppKit)
descriptor = descriptor.withSymbolicTraits(symbolicTraits)
#else
descriptor = descriptor.withSymbolicTraits(symbolicTraits)!
#endif
func verticalPositionFeature(forSuperscript: Bool) -> [FontDescriptor.FeatureKey: Any] {
#if canImport(AppKit)
let features: [FontDescriptor.FeatureKey: Any] = [.typeIdentifier: kVerticalPositionType, .selectorIdentifier: forSuperscript ? kSuperiorsSelector : kInferiorsSelector]
#else
let features: [FontDescriptor.FeatureKey: Any] = [.featureIdentifier: kVerticalPositionType, .typeIdentifier: forSuperscript ? kSuperiorsSelector : kInferiorsSelector]
#endif
return features
}
if styles.contains(.superscript) || styles.contains(.subscript) {
let features = verticalPositionFeature(forSuperscript: styles.contains(.superscript))
let descriptorAttributes: [FontDescriptor.AttributeName: Any] = [.featureSettings: [features]]
descriptor = descriptor.addingAttributes(descriptorAttributes)
}
var attributes = [NSAttributedString.Key: Any]()
attributes[.font] = Font(descriptor: descriptor, size: baseFont.pointSize)
if styles.contains(.strikethrough) {
attributes[.strikethroughStyle] = NSUnderlineStyle.single.rawValue
}
if styles.contains(.underline) {
attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue
}
result.addAttributes(attributes, range: range)
}
self.init(attributedString: result)
}
}
/// This is a very, very basic implementation that only covers our needs.
private struct CountedSet<Element> where Element: Hashable {
private var _storage = [Element: Int]()
mutating func insert(_ element: Element) {
_storage[element, default: 0] += 1
}
mutating func remove(_ element: Element) {
guard var count = _storage[element] else { return }
count -= 1
if count == 0 {
_storage.removeValue(forKey: element)
} else {
_storage[element] = count
}
}
func contains(_ element: Element) -> Bool {
return _storage[element] != nil
}
subscript(key: Element) -> Int {
get {
return _storage[key, default: 0]
}
}
}
private extension String {
var decodedEntity: String {
// It's possible the implementation will change, but for now it just calls this.
(self as NSString).rsparser_stringByDecodingHTMLEntities() as String
}
}

View File

@ -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 = ""

View File

@ -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 {
s = s.adding(font: fieldFont)
}
if label.attributedText != s {
label.attributedText = s
setNeedsLayout()
}
}
func updateFeedNameView() {
switch cellData.showFeedName {

@ -1 +1 @@
Subproject commit 0ba28167babdd4710adc1764ed49979c9eb42ead
Subproject commit 3dfa570a4600690290cd946b8e122b0b99da0a13

@ -1 +1 @@
Subproject commit 0a8718c5c412585141d8374ae01b1e9e75dd1019
Subproject commit de7753914c4f47aa97adfbe7a121acf0d2f8cd40

@ -1 +1 @@
Subproject commit 8c035b26767e66f5639c2fc0f3398216a46cb3d1
Subproject commit fcbd9a34ecd8c080c6f26798a4b22ea0c98d8e74

@ -1 +1 @@
Subproject commit 81011808b6c4242cbf95cb55120f250c157d594c
Subproject commit 2fc9b9cff60032a272303ff6d6df5b39ec297179

@ -1 +1 @@
Subproject commit 7af4393f575479d249b83e7761a59d14b6c170d0
Subproject commit 05388e4f7073b014f786cfce18782c3d61f8e378