diff options
Diffstat (limited to 'Sources/NorgEditor/EditorTheme.swift')
| -rw-r--r-- | Sources/NorgEditor/EditorTheme.swift | 187 |
1 files changed, 187 insertions, 0 deletions
diff --git a/Sources/NorgEditor/EditorTheme.swift b/Sources/NorgEditor/EditorTheme.swift new file mode 100644 index 0000000..2ceb596 --- /dev/null +++ b/Sources/NorgEditor/EditorTheme.swift @@ -0,0 +1,187 @@ +#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 |