aboutsummaryrefslogtreecommitdiff
path: root/Sources/NorgEditor/EditorTheme.swift
blob: 2ceb596811c88ac63ed4830159374093729a3a88 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
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