task 5: add AttributedStringBuilder for platform-aware font attributes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
121
VorleserKit/Sources/BookParser/AttributedStringBuilder.swift
Normal file
121
VorleserKit/Sources/BookParser/AttributedStringBuilder.swift
Normal file
@@ -0,0 +1,121 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user