Files
vorleser/VorleserKit/Sources/BookParser/AttributedStringBuilder.swift

122 lines
3.5 KiB
Swift

import Foundation
#if canImport(UIKit)
import UIKit
#elseif canImport(AppKit)
import AppKit
#endif
/// Builds an NSAttributedString from plain text with formatting ranges.
/// The resulting .string is character-for-character identical to the input text.
enum AttributedStringBuilder {
struct FormattingRange {
enum Style {
case body
case bold
case italic
case heading(level: Int)
case paragraphStart
}
let range: NSRange
let style: Style
}
/// Build an attributed string from plain text and formatting ranges.
/// The plain text is used as-is no characters are added or removed.
static func build(text: String, ranges: [FormattingRange]) -> NSAttributedString {
let result = NSMutableAttributedString(
string: text,
attributes: [
.font: bodyFont(),
.foregroundColor: textColor(),
]
)
for range in ranges {
switch range.style {
case .body:
break
case .bold:
result.addAttribute(.font, value: boldFont(), range: range.range)
case .italic:
result.addAttribute(.font, value: italicFont(), range: range.range)
case .heading(let level):
result.addAttribute(.font, value: headingFont(level: level), range: range.range)
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.paragraphSpacingBefore = 16
result.addAttribute(.paragraphStyle, value: paragraphStyle, range: range.range)
case .paragraphStart:
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.paragraphSpacingBefore = 8
result.addAttribute(.paragraphStyle, value: paragraphStyle, range: range.range)
}
}
return NSAttributedString(attributedString: result)
}
/// Build a plain attributed string with body font (no formatting ranges).
static func buildPlain(text: String) -> NSAttributedString {
NSAttributedString(
string: text,
attributes: [
.font: bodyFont(),
.foregroundColor: textColor(),
]
)
}
// MARK: - Platform fonts
#if canImport(UIKit)
private static func bodyFont() -> UIFont {
.preferredFont(forTextStyle: .body)
}
private static func boldFont() -> UIFont {
let descriptor = UIFont.preferredFont(forTextStyle: .body).fontDescriptor.withSymbolicTraits(.traitBold)!
return UIFont(descriptor: descriptor, size: 0)
}
private static func italicFont() -> UIFont {
let descriptor = UIFont.preferredFont(forTextStyle: .body).fontDescriptor.withSymbolicTraits(.traitItalic)!
return UIFont(descriptor: descriptor, size: 0)
}
private static func headingFont(level: Int) -> UIFont {
let style: UIFont.TextStyle = level <= 2 ? .title1 : .title3
let descriptor = UIFont.preferredFont(forTextStyle: style).fontDescriptor.withSymbolicTraits(.traitBold)!
return UIFont(descriptor: descriptor, size: 0)
}
private static func textColor() -> UIColor {
.label
}
#elseif canImport(AppKit)
private static func bodyFont() -> NSFont {
.preferredFont(forTextStyle: .body)
}
private static func boldFont() -> NSFont {
let body = NSFont.preferredFont(forTextStyle: .body)
return NSFontManager.shared.convert(body, toHaveTrait: .boldFontMask)
}
private static func italicFont() -> NSFont {
let body = NSFont.preferredFont(forTextStyle: .body)
return NSFontManager.shared.convert(body, toHaveTrait: .italicFontMask)
}
private static func headingFont(level: Int) -> NSFont {
let style: NSFont.TextStyle = level <= 2 ? .title1 : .title3
let heading = NSFont.preferredFont(forTextStyle: style)
return NSFontManager.shared.convert(heading, toHaveTrait: .boldFontMask)
}
private static func textColor() -> NSColor {
.textColor
}
#endif
}