// // 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. /// - 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) if let color = color { mutable.addAttribute(.foregroundColor, value: color as Any, range: fullRange) } let size = baseFont.pointSize let baseDescriptor = baseFont.fontDescriptor let baseSymbolicTraits = baseDescriptor.symbolicTraits 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? Font else { return } var newSymbolicTraits = baseSymbolicTraits let symbolicTraits = font.fontDescriptor.symbolicTraits newSymbolicTraits.insert(symbolicTraits) var descriptor = baseDescriptor.addingAttributes(font.fontDescriptor.fontAttributes) #if canImport(AppKit) descriptor = descriptor.withSymbolicTraits(newSymbolicTraits) #else var descriptor = descriptor.withSymbolicTraits(newSymbolicTraits)! #endif 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: [FontDescriptor.TraitKey: Any] = [.weight: Font.Weight.heavy] let attributes: [FontDescriptor.AttributeName: Any] = [.traits: traits] descriptor = descriptor.addingAttributes(attributes) } } let newFont = Font(descriptor: descriptor, size: size) mutable.addAttribute(.font, value: newFont as Any, range: range) } return mutable.copy() as! NSAttributedString } /// Creates an attributed string from HTML. 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?(tag: String) { switch tag { case "b", "strong": self = .bold case "i", "em", "cite": 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 `` tags. convenience init(html: String, locale: Locale = Locale.current) { let baseFont = Font.systemFont(ofSize: Font.systemFontSize) var inTag: InTag = .none var tag = "" var tagStack = [String]() var currentStyles = CountedSet