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 }