122 lines
3.5 KiB
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
|
|
}
|