aboutsummaryrefslogtreecommitdiff
path: root/Sources/NorgEditor/EditorTheme.swift
diff options
context:
space:
mode:
Diffstat (limited to 'Sources/NorgEditor/EditorTheme.swift')
-rw-r--r--Sources/NorgEditor/EditorTheme.swift187
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