Add attributed title support in the timeline
This commit is contained in:
parent
4ddb6c8d4f
commit
3d509a94d4
|
@ -34,6 +34,7 @@ final class MultilineTextFieldSizer {
|
||||||
private let singleLineHeightEstimate: Int
|
private let singleLineHeightEstimate: Int
|
||||||
private let doubleLineHeightEstimate: Int
|
private let doubleLineHeightEstimate: Int
|
||||||
private var cache = [String: WidthHeightCache]() // Each string has a cache.
|
private var cache = [String: WidthHeightCache]() // Each string has a cache.
|
||||||
|
private var attributedCache = [NSAttributedString: WidthHeightCache]()
|
||||||
private static var sizers = [TextFieldSizerSpecifier: MultilineTextFieldSizer]()
|
private static var sizers = [TextFieldSizerSpecifier: MultilineTextFieldSizer]()
|
||||||
|
|
||||||
private init(numberOfLines: Int, font: NSFont) {
|
private init(numberOfLines: Int, font: NSFont) {
|
||||||
|
@ -51,6 +52,14 @@ final class MultilineTextFieldSizer {
|
||||||
return sizer(numberOfLines: numberOfLines, font: font).sizeInfo(for: string, width: width)
|
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() {
|
static func emptyCache() {
|
||||||
|
|
||||||
sizers = [TextFieldSizerSpecifier: MultilineTextFieldSizer]()
|
sizers = [TextFieldSizerSpecifier: MultilineTextFieldSizer]()
|
||||||
|
@ -83,6 +92,16 @@ private extension MultilineTextFieldSizer {
|
||||||
return sizeInfo
|
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 {
|
func height(for string: String, width: Int) -> Int {
|
||||||
|
|
||||||
if cache[string] == nil {
|
if cache[string] == nil {
|
||||||
|
@ -103,6 +122,26 @@ private extension MultilineTextFieldSizer {
|
||||||
return height
|
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 {
|
static func createTextField(_ numberOfLines: Int, _ font: NSFont) -> NSTextField {
|
||||||
|
|
||||||
let textField = NSTextField(wrappingLabelWithString: "")
|
let textField = NSTextField(wrappingLabelWithString: "")
|
||||||
|
@ -120,6 +159,14 @@ private extension MultilineTextFieldSizer {
|
||||||
return MultilineTextFieldSizer.calculateHeight(string, width, textField)
|
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 {
|
static func calculateHeight(_ string: String, _ width: Int, _ textField: NSTextField) -> Int {
|
||||||
|
|
||||||
textField.stringValue = string
|
textField.stringValue = string
|
||||||
|
|
|
@ -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<ObjCBool>) 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<ObjCBool>) 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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -12,6 +12,7 @@ import Articles
|
||||||
struct TimelineCellData {
|
struct TimelineCellData {
|
||||||
|
|
||||||
let title: String
|
let title: String
|
||||||
|
let attributedTitle: NSAttributedString
|
||||||
let text: String
|
let text: String
|
||||||
let dateString: String
|
let dateString: String
|
||||||
let feedName: 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?) {
|
init(article: Article, showFeedName: TimelineShowFeedName, feedName: String?, byline: String?, iconImage: IconImage?, showIcon: Bool, featuredImage: NSImage?) {
|
||||||
|
|
||||||
self.title = ArticleStringFormatter.truncatedTitle(article)
|
self.title = ArticleStringFormatter.truncatedTitle(article)
|
||||||
|
self.attributedTitle = ArticleStringFormatter.attributedTruncatedTitle(article)
|
||||||
self.text = ArticleStringFormatter.truncatedSummary(article)
|
self.text = ArticleStringFormatter.truncatedSummary(article)
|
||||||
|
|
||||||
self.dateString = ArticleStringFormatter.dateString(article.logicalDatePublished)
|
self.dateString = ArticleStringFormatter.dateString(article.logicalDatePublished)
|
||||||
|
@ -64,5 +66,6 @@ struct TimelineCellData {
|
||||||
self.featuredImage = nil
|
self.featuredImage = nil
|
||||||
self.read = true
|
self.read = true
|
||||||
self.starred = false
|
self.starred = false
|
||||||
|
self.attributedTitle = NSAttributedString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -115,7 +115,8 @@ private extension TimelineCellLayout {
|
||||||
return (r, 0)
|
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
|
r.size.height = sizeInfo.size.height
|
||||||
if sizeInfo.numberOfLinesUsed < 1 {
|
if sizeInfo.numberOfLinesUsed < 1 {
|
||||||
r.size.height = 0
|
r.size.height = 0
|
||||||
|
|
|
@ -222,6 +222,7 @@ private extension TimelineTableCellView {
|
||||||
func updateTitleView() {
|
func updateTitleView() {
|
||||||
|
|
||||||
updateTextFieldText(titleView, cellData?.title)
|
updateTextFieldText(titleView, cellData?.title)
|
||||||
|
updateTextFieldAttributedText(titleView, cellData?.attributedTitle)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateSummaryView() {
|
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() {
|
func updateFeedNameView() {
|
||||||
switch cellData.showFeedName {
|
switch cellData.showFeedName {
|
||||||
case .byline:
|
case .byline:
|
||||||
|
|
|
@ -725,6 +725,8 @@
|
||||||
84FF69B11FC3793300DC198E /* FaviconURLFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FF69B01FC3793300DC198E /* FaviconURLFinder.swift */; };
|
84FF69B11FC3793300DC198E /* FaviconURLFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FF69B01FC3793300DC198E /* FaviconURLFinder.swift */; };
|
||||||
B27EEBF9244D15F3000932E6 /* shared.css in Resources */ = {isa = PBXBuildFile; fileRef = B27EEBDF244D15F2000932E6 /* shared.css */; };
|
B27EEBF9244D15F3000932E6 /* shared.css in Resources */ = {isa = PBXBuildFile; fileRef = B27EEBDF244D15F2000932E6 /* shared.css */; };
|
||||||
B27EEBFA244D15F3000932E6 /* 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 */; };
|
B27EEBFB244D15F3000932E6 /* shared.css in Resources */ = {isa = PBXBuildFile; fileRef = B27EEBDF244D15F2000932E6 /* shared.css */; };
|
||||||
B2B8075E239C49D300F191E0 /* RSImage-AppIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B8075D239C49D300F191E0 /* RSImage-AppIcons.swift */; };
|
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 */; };
|
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 = "<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>"; };
|
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>"; };
|
B27EEBDF244D15F2000932E6 /* shared.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = shared.css; sourceTree = "<group>"; };
|
||||||
|
B26B9571243D176B0053EEF5 /* NSAttributedString+NetNewsWire.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+NetNewsWire.swift"; sourceTree = "<group>"; };
|
||||||
B2B8075D239C49D300F191E0 /* RSImage-AppIcons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RSImage-AppIcons.swift"; sourceTree = "<group>"; };
|
B2B8075D239C49D300F191E0 /* RSImage-AppIcons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RSImage-AppIcons.swift"; sourceTree = "<group>"; };
|
||||||
B528F81D23333C7E00E735DD /* page.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = page.html; sourceTree = "<group>"; };
|
B528F81D23333C7E00E735DD /* page.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = page.html; sourceTree = "<group>"; };
|
||||||
BDCB514D24282C8A00102A80 /* AccountsNewsBlur.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = AccountsNewsBlur.xib; sourceTree = "<group>"; };
|
BDCB514D24282C8A00102A80 /* AccountsNewsBlur.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = AccountsNewsBlur.xib; sourceTree = "<group>"; };
|
||||||
|
@ -2626,6 +2629,7 @@
|
||||||
84E185C2203BB12600F69BFA /* MultilineTextFieldSizer.swift */,
|
84E185C2203BB12600F69BFA /* MultilineTextFieldSizer.swift */,
|
||||||
849A97711ED9EC04007D329B /* TimelineCellData.swift */,
|
849A97711ED9EC04007D329B /* TimelineCellData.swift */,
|
||||||
849A97751ED9EC04007D329B /* UnreadIndicatorView.swift */,
|
849A97751ED9EC04007D329B /* UnreadIndicatorView.swift */,
|
||||||
|
B26B9571243D176B0053EEF5 /* NSAttributedString+NetNewsWire.swift */,
|
||||||
);
|
);
|
||||||
path = Cell;
|
path = Cell;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -4206,6 +4210,7 @@
|
||||||
65ED4023235DEF6C0081F399 /* Folder+Scriptability.swift in Sources */,
|
65ED4023235DEF6C0081F399 /* Folder+Scriptability.swift in Sources */,
|
||||||
65ED4024235DEF6C0081F399 /* TimelineCellLayout.swift in Sources */,
|
65ED4024235DEF6C0081F399 /* TimelineCellLayout.swift in Sources */,
|
||||||
65ED4025235DEF6C0081F399 /* DetailWebView.swift in Sources */,
|
65ED4025235DEF6C0081F399 /* DetailWebView.swift in Sources */,
|
||||||
|
B26B9573243D176B0053EEF5 /* NSAttributedString+NetNewsWire.swift in Sources */,
|
||||||
B2B80779239C4C7300F191E0 /* RSImage-AppIcons.swift in Sources */,
|
B2B80779239C4C7300F191E0 /* RSImage-AppIcons.swift in Sources */,
|
||||||
65ED4026235DEF6C0081F399 /* TimelineTableRowView.swift in Sources */,
|
65ED4026235DEF6C0081F399 /* TimelineTableRowView.swift in Sources */,
|
||||||
65ED4027235DEF6C0081F399 /* UnreadIndicatorView.swift in Sources */,
|
65ED4027235DEF6C0081F399 /* UnreadIndicatorView.swift in Sources */,
|
||||||
|
@ -4541,6 +4546,7 @@
|
||||||
8426119E1FCB6ED40086A189 /* HTMLMetadataDownloader.swift in Sources */,
|
8426119E1FCB6ED40086A189 /* HTMLMetadataDownloader.swift in Sources */,
|
||||||
849A976E1ED9EBC8007D329B /* TimelineViewController.swift in Sources */,
|
849A976E1ED9EBC8007D329B /* TimelineViewController.swift in Sources */,
|
||||||
3B826DCC2385C84800FC1ADB /* AccountsFeedWranglerWindowController.swift in Sources */,
|
3B826DCC2385C84800FC1ADB /* AccountsFeedWranglerWindowController.swift in Sources */,
|
||||||
|
B26B9572243D176B0053EEF5 /* NSAttributedString+NetNewsWire.swift in Sources */,
|
||||||
5154368B229404D1005E1CDF /* FaviconGenerator.swift in Sources */,
|
5154368B229404D1005E1CDF /* FaviconGenerator.swift in Sources */,
|
||||||
5183CCE6226F4E110010922C /* RefreshInterval.swift in Sources */,
|
5183CCE6226F4E110010922C /* RefreshInterval.swift in Sources */,
|
||||||
849A97771ED9EC04007D329B /* TimelineCellData.swift in Sources */,
|
849A97771ED9EC04007D329B /* TimelineCellData.swift in Sources */,
|
||||||
|
|
|
@ -46,7 +46,7 @@ struct ArticleRenderer {
|
||||||
self.article = article
|
self.article = article
|
||||||
self.extractedArticle = extractedArticle
|
self.extractedArticle = extractedArticle
|
||||||
self.articleStyle = style
|
self.articleStyle = style
|
||||||
self.title = article?.title ?? ""
|
self.title = article?.sanitizedTitle() ?? ""
|
||||||
if let content = extractedArticle?.content {
|
if let content = extractedArticle?.content {
|
||||||
self.body = content
|
self.body = content
|
||||||
self.baseURL = extractedArticle?.url
|
self.baseURL = extractedArticle?.url
|
||||||
|
|
|
@ -52,8 +52,8 @@ struct ArticleStringFormatter {
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
static func truncatedTitle(_ article: Article) -> String {
|
static func truncatedTitle(_ article: Article, forHTML: Bool = false) -> String {
|
||||||
guard let title = article.title else {
|
guard let title = article.sanitizedTitle(forHTML: forHTML) else {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,7 +64,11 @@ struct ArticleStringFormatter {
|
||||||
var s = title.replacingOccurrences(of: "\n", with: "")
|
var s = title.replacingOccurrences(of: "\n", with: "")
|
||||||
s = s.replacingOccurrences(of: "\r", with: "")
|
s = s.replacingOccurrences(of: "\r", with: "")
|
||||||
s = s.replacingOccurrences(of: "\t", with: "")
|
s = s.replacingOccurrences(of: "\t", with: "")
|
||||||
s = s.rsparser_stringByDecodingHTMLEntities()
|
|
||||||
|
if !forHTML {
|
||||||
|
s = s.rsparser_stringByDecodingHTMLEntities()
|
||||||
|
}
|
||||||
|
|
||||||
s = s.trimmingWhitespace
|
s = s.trimmingWhitespace
|
||||||
s = s.collapsingWhitespace
|
s = s.collapsingWhitespace
|
||||||
|
|
||||||
|
@ -79,6 +83,13 @@ struct ArticleStringFormatter {
|
||||||
return s
|
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 {
|
static func truncatedSummary(_ article: Article) -> String {
|
||||||
guard let body = article.body else {
|
guard let body = article.body else {
|
||||||
return ""
|
return ""
|
||||||
|
|
Loading…
Reference in New Issue