#if canImport(UIKit) import UIKit public typealias PlatformFont = UIFont public typealias PlatformColor = UIColor #elseif canImport(AppKit) import AppKit public typealias PlatformFont = NSFont public typealias PlatformColor = NSColor #endif #if canImport(UIKit) || canImport(AppKit) import NorgKit /// Typography and colours used when highlighting a Norg document. public struct EditorTheme { /// Base font for source text. Inline `bold`/`italic` runs derive bold and /// italic variants from this font's descriptor. public var font: PlatformFont /// Base foreground colour for plain, unstyled source. public var textColor: PlatformColor /// Color for a heading marker run (`*`…) at the given 1-based level. public var headingColor: @Sendable (Int) -> PlatformColor /// Color for structural markers and delimiters: list/quote/definition/ /// footnote/table-cell runs, delimiting lines, rules, inline-modifier /// delimiters, link brackets and escapes. public var markerColor: PlatformColor /// Color for a task status marker, e.g. `(x)`. public var taskStatusColor: @Sendable (TaskStatus) -> PlatformColor /// Color for a ranged-tag header: the `@` and the tag name/parameters. public var tagColor: PlatformColor /// Color for verbatim content: inline `` `verbatim` `` and ranged-tag /// (`@code` … `@end`) body lines. public var verbatimColor: PlatformColor /// Color for inline comments (`%…%`). public var commentColor: PlatformColor /// Color for link/anchor locations and descriptions. public var linkColor: PlatformColor /// Color for `spoiler` inline runs. public var spoilerColor: PlatformColor public init( font: PlatformFont, textColor: PlatformColor, headingColor: @escaping @Sendable (Int) -> PlatformColor, markerColor: PlatformColor, taskStatusColor: @escaping @Sendable (TaskStatus) -> PlatformColor, tagColor: PlatformColor, verbatimColor: PlatformColor, commentColor: PlatformColor, linkColor: PlatformColor, spoilerColor: PlatformColor ) { self.font = font self.textColor = textColor self.headingColor = headingColor self.markerColor = markerColor self.taskStatusColor = taskStatusColor self.tagColor = tagColor self.verbatimColor = verbatimColor self.commentColor = commentColor self.linkColor = linkColor self.spoilerColor = spoilerColor } public static let `default` = EditorTheme( font: .monospacedSystemFont(ofSize: 16, weight: .regular), textColor: .norgLabel, headingColor: { _ in .systemBlue }, markerColor: .norgSecondaryLabel, taskStatusColor: { status in switch status { case .done, .recurring: return .systemGreen case .urgent: return .systemRed case .pending, .onHold: return .systemBlue case .cancelled: return .norgTertiaryLabel case .undone, .needsInput: return .norgSecondaryLabel } }, tagColor: .systemPurple, verbatimColor: .systemPink, commentColor: .norgTertiaryLabel, linkColor: .norgLink, spoilerColor: .norgSecondaryLabel ) } #if canImport(UIKit) extension EditorTheme: Sendable {} #elseif canImport(AppKit) extension EditorTheme: @unchecked Sendable {} #endif extension EditorTheme { public func attributes(for kind: NorgToken.Kind) -> [NSAttributedString.Key: Any] { switch kind { case .heading(let level): return [.foregroundColor: headingColor(level)] case .unorderedList, .orderedList, .quote, .definition, .footnote, .tableCell, .weakDelimiter, .strongDelimiter, .horizontalRule, .modifierDelimiter, .linkDelimiter, .escape: return [.foregroundColor: markerColor] case .taskStatus(let status): return [.foregroundColor: taskStatusColor(status)] case .tagDelimiter, .tagName: return [.foregroundColor: tagColor] case .verbatimBlock: return [.foregroundColor: verbatimColor] case .comment: return [.foregroundColor: commentColor] case .linkTarget: return [ .foregroundColor: linkColor, .underlineStyle: NSUnderlineStyle.single.rawValue, ] case .linkDescription: return [.foregroundColor: linkColor] case .styledText(let style): return styledAttributes(style) } } private func styledAttributes(_ style: InlineStyle) -> [NSAttributedString.Key: Any] { var attributes: [NSAttributedString.Key: Any] = [:] if let traited = traitedFont(bold: style.contains(.bold), italic: style.contains(.italic)) { attributes[.font] = traited } if style.contains(.underline) { attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue } if style.contains(.strikethrough) { attributes[.strikethroughStyle] = NSUnderlineStyle.single.rawValue } if style.contains(.verbatim) || style.contains(.math) { attributes[.foregroundColor] = verbatimColor } else if style.contains(.spoiler) { attributes[.foregroundColor] = spoilerColor } return attributes } private func traitedFont(bold: Bool, italic: Bool) -> PlatformFont? { guard bold || italic else { return nil } #if canImport(UIKit) var traits: UIFontDescriptor.SymbolicTraits = [] if bold { traits.insert(.traitBold) } if italic { traits.insert(.traitItalic) } guard let descriptor = font.fontDescriptor.withSymbolicTraits(traits) else { return nil } return UIFont(descriptor: descriptor, size: font.pointSize) #elseif canImport(AppKit) var traits: NSFontDescriptor.SymbolicTraits = [] if bold { traits.insert(.bold) } if italic { traits.insert(.italic) } let descriptor = font.fontDescriptor.withSymbolicTraits(traits) return NSFont(descriptor: descriptor, size: font.pointSize) #endif } public func applyHighlighting(to storage: NSTextStorage) { let text = storage.string let full = NSRange(location: 0, length: (text as NSString).length) storage.setAttributes([.font: font, .foregroundColor: textColor], range: full) for highlight in NorgHighlighter.highlights(in: text) { storage.addAttributes(attributes(for: highlight.kind), range: highlight.range) } } } extension PlatformColor { #if canImport(UIKit) fileprivate static var norgLabel: PlatformColor { .label } fileprivate static var norgSecondaryLabel: PlatformColor { .secondaryLabel } fileprivate static var norgTertiaryLabel: PlatformColor { .tertiaryLabel } fileprivate static var norgLink: PlatformColor { .link } #elseif canImport(AppKit) fileprivate static var norgLabel: PlatformColor { .labelColor } fileprivate static var norgSecondaryLabel: PlatformColor { .secondaryLabelColor } fileprivate static var norgTertiaryLabel: PlatformColor { .tertiaryLabelColor } fileprivate static var norgLink: PlatformColor { .linkColor } #endif } #endif