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
#endif
#if os(iOS)
private typealias PlatformFont = UIFont
#elseif os(macOS)
private typealias PlatformFont = NSFont
#endif
public struct AttributedStringConverter<Callbacks: AttributedStringCallbacks> {
private let configuration: AttributedStringConverterConfiguration
private var tokenizer: Tokenizer<String.Iterator>
private let str = NSMutableAttributedString()
private var fontCache: [FontTrait: PlatformFont] = [:]
private var tokenizer: Tokenizer<String.Iterator>!
private var str: NSMutableAttributedString!
private var actionStack: InlineArray3<ElementAction> = []
private var styleStack: InlineArray3<Style> = []
// The current run of text w/o styles changing
private var currentRun: String = ""
public init(html: String, configuration: AttributedStringConverterConfiguration) where Callbacks == DefaultCallbacks {
self.init(html: html, configuration: configuration, callbacks: DefaultCallbacks.self)
public init(configuration: AttributedStringConverterConfiguration) where Callbacks == DefaultCallbacks {
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.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() {
switch token {
case .character(let c):
@ -215,7 +229,7 @@ public struct AttributedStringConverter<Callbacks: AttributedStringCallbacks> {
}
var attributes = [NSAttributedString.Key: Any]()
var currentFontTraits = Set<FontTrait>()
var currentFontTraits: FontTrait = []
for style in styleStack {
switch style {
case .bold:
@ -238,23 +252,7 @@ public struct AttributedStringConverter<Callbacks: AttributedStringCallbacks> {
}
}
let baseFont = currentFontTraits.contains(.monospace) ? configuration.monospaceFont : configuration.font
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
attributes[.font] = getFont(traits: currentFontTraits)
if !attributes.keys.contains(.paragraphStyle) {
attributes[.paragraphStyle] = configuration.paragraphStyle
@ -263,6 +261,28 @@ public struct AttributedStringConverter<Callbacks: AttributedStringCallbacks> {
str.append(NSAttributedString(string: currentRun, attributes: attributes))
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 {
@ -338,10 +358,16 @@ private extension NSFontDescriptor.SymbolicTraits {
}
#endif
private enum FontTrait {
case bold
case italic
case monospace
private struct FontTrait: OptionSet, Hashable {
static let bold = FontTrait(rawValue: 1 << 0)
static let italic = FontTrait(rawValue: 1 << 1)
static let monospace = FontTrait(rawValue: 1 << 2)
let rawValue: Int
init(rawValue: Int) {
self.rawValue = rawValue
}
}
private enum Style {

View File

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