Cache fonts in attributed string converter

This commit is contained in:
Shadowfacts 2023-11-26 18:53:59 -05:00
parent 134803b72d
commit 1c461041c1
2 changed files with 57 additions and 31 deletions

View File

@ -11,26 +11,40 @@ import UIKit
import AppKit import AppKit
#endif #endif
#if os(iOS)
private typealias PlatformFont = UIFont
#elseif os(macOS)
private typealias PlatformFont = NSFont
#endif
public struct AttributedStringConverter<Callbacks: AttributedStringCallbacks> { public struct AttributedStringConverter<Callbacks: AttributedStringCallbacks> {
private let configuration: AttributedStringConverterConfiguration private let configuration: AttributedStringConverterConfiguration
private var tokenizer: Tokenizer<String.Iterator> private var fontCache: [FontTrait: PlatformFont] = [:]
private let str = NSMutableAttributedString()
private var tokenizer: Tokenizer<String.Iterator>!
private var str: NSMutableAttributedString!
private var actionStack: InlineArray3<ElementAction> = [] private var actionStack: InlineArray3<ElementAction> = []
private var styleStack: InlineArray3<Style> = [] private var styleStack: InlineArray3<Style> = []
// The current run of text w/o styles changing // The current run of text w/o styles changing
private var currentRun: String = "" private var currentRun: String = ""
public init(html: String, configuration: AttributedStringConverterConfiguration) where Callbacks == DefaultCallbacks { public init(configuration: AttributedStringConverterConfiguration) where Callbacks == DefaultCallbacks {
self.init(html: html, configuration: configuration, callbacks: DefaultCallbacks.self) self.init(configuration: configuration, callbacks: DefaultCallbacks.self)
} }
public init(html: String, configuration: AttributedStringConverterConfiguration, callbacks _: Callbacks.Type = Callbacks.self) { public init(configuration: AttributedStringConverterConfiguration, callbacks _: Callbacks.Type = Callbacks.self) {
self.configuration = configuration self.configuration = configuration
self.tokenizer = Tokenizer(chars: html.makeIterator())
} }
public mutating func convert() -> NSAttributedString { public mutating func convert(html: String) -> NSAttributedString {
tokenizer = Tokenizer(chars: html.makeIterator())
str = NSMutableAttributedString()
actionStack = []
styleStack = []
currentRun = ""
while let token = tokenizer.next() { while let token = tokenizer.next() {
switch token { switch token {
case .character(let c): case .character(let c):
@ -215,7 +229,7 @@ public struct AttributedStringConverter<Callbacks: AttributedStringCallbacks> {
} }
var attributes = [NSAttributedString.Key: Any]() var attributes = [NSAttributedString.Key: Any]()
var currentFontTraits = Set<FontTrait>() var currentFontTraits: FontTrait = []
for style in styleStack { for style in styleStack {
switch style { switch style {
case .bold: case .bold:
@ -238,23 +252,7 @@ public struct AttributedStringConverter<Callbacks: AttributedStringCallbacks> {
} }
} }
let baseFont = currentFontTraits.contains(.monospace) ? configuration.monospaceFont : configuration.font attributes[.font] = getFont(traits: currentFontTraits)
var descriptor = baseFont.fontDescriptor
if currentFontTraits.contains(.bold) && currentFontTraits.contains(.italic),
let boldItalic = descriptor.withSymbolicTraits([.traitBold, .traitItalic]) {
descriptor = boldItalic
} else if currentFontTraits.contains(.bold),
let bold = descriptor.withSymbolicTraits(.traitBold) {
descriptor = bold
} else if currentFontTraits.contains(.italic),
let italic = descriptor.withSymbolicTraits(.traitItalic) {
descriptor = italic
}
#if os(iOS)
attributes[.font] = UIFont(descriptor: descriptor, size: 0)
#elseif os(macOS)
attributes[.font] = NSFont(descriptor: descriptor, size: 0)
#endif
if !attributes.keys.contains(.paragraphStyle) { if !attributes.keys.contains(.paragraphStyle) {
attributes[.paragraphStyle] = configuration.paragraphStyle attributes[.paragraphStyle] = configuration.paragraphStyle
@ -263,6 +261,28 @@ public struct AttributedStringConverter<Callbacks: AttributedStringCallbacks> {
str.append(NSAttributedString(string: currentRun, attributes: attributes)) str.append(NSAttributedString(string: currentRun, attributes: attributes))
currentRun = "" currentRun = ""
} }
private mutating func getFont(traits: FontTrait) -> PlatformFont? {
if let cached = fontCache[traits] {
return cached
}
let baseFont = traits.contains(.monospace) ? configuration.monospaceFont : configuration.font
var descriptor = baseFont.fontDescriptor
if traits.contains(.bold) && traits.contains(.italic),
let boldItalic = descriptor.withSymbolicTraits([.traitBold, .traitItalic]) {
descriptor = boldItalic
} else if traits.contains(.bold),
let bold = descriptor.withSymbolicTraits(.traitBold) {
descriptor = bold
} else if traits.contains(.italic),
let italic = descriptor.withSymbolicTraits(.traitItalic) {
descriptor = italic
}
let font = PlatformFont(descriptor: descriptor, size: 0)
fontCache[traits] = font
return font
}
} }
public protocol AttributedStringCallbacks { public protocol AttributedStringCallbacks {
@ -338,10 +358,16 @@ private extension NSFontDescriptor.SymbolicTraits {
} }
#endif #endif
private enum FontTrait { private struct FontTrait: OptionSet, Hashable {
case bold static let bold = FontTrait(rawValue: 1 << 0)
case italic static let italic = FontTrait(rawValue: 1 << 1)
case monospace static let monospace = FontTrait(rawValue: 1 << 2)
let rawValue: Int
init(rawValue: Int) {
self.rawValue = rawValue
}
} }
private enum Style { private enum Style {

View File

@ -45,8 +45,8 @@ final class AttributedStringConverterTests: XCTestCase {
color: .black, color: .black,
paragraphStyle: .default paragraphStyle: .default
) )
var converter = AttributedStringConverter<Callbacks>(html: html, configuration: config) var converter = AttributedStringConverter<Callbacks>(configuration: config)
return converter.convert() return converter.convert(html: html)
} }
func testConvertBR() { func testConvertBR() {