aboutsummaryrefslogtreecommitdiff
path: root/Sources/NorgUI/Extensions/AttributedString+Norg.swift
blob: 286e0c81beffa30962cb922d85f03dd462af0348 (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
import NorgKit
import SwiftUI

/// Extensions to AttributedString to generate from NorgKit models.
extension AttributedString {

  /// Builds a styled attributed string from parsed Norg inline spans.
  public init(norg spans: [InlineSpan], baseFont: Font = .body, theme: NorgTheme = .default) {
    var result = AttributedString()
    for span in spans {
      result.append(Self.render(span, baseFont: baseFont, theme: theme))
    }
    self = result
  }

  private static func render(_ span: InlineSpan, baseFont: Font, theme: NorgTheme)
    -> AttributedString {
    var run = AttributedString(span.text)
    run.font = font(for: span.styles, baseFont: baseFont)
    applyDecorations(to: &run, styles: span.styles, theme: theme)
    if let link = span.link {
      applyLink(link, to: &run, theme: theme)
    }
    return run
  }

  private static func font(for styles: InlineStyle, baseFont: Font) -> Font {
    var font = baseFont
    if styles.contains(.verbatim) || styles.contains(.math) { font = font.monospaced() }
    if styles.contains(.bold) { font = font.bold() }
    if styles.contains(.italic) { font = font.italic() }
    return font
  }

  private static func applyDecorations(
    to run: inout AttributedString, styles: InlineStyle, theme: NorgTheme
  ) {
    if styles.contains(.underline) { run.underlineStyle = .single }
    if styles.contains(.strikethrough) { run.strikethroughStyle = .single }
    if styles.contains(.superscript) { run.baselineOffset = theme.superscriptOffset }
    if styles.contains(.subscript) { run.baselineOffset = theme.subscriptOffset }
    if styles.contains(.spoiler) {
      run.foregroundColor = theme.spoilerForeground
      run.backgroundColor = theme.spoilerBackground
    }
    if styles.contains(.verbatim) {
      run.foregroundColor = theme.verbatimColor
    }
  }

  private static func applyLink(
    _ link: InlineLink, to run: inout AttributedString, theme: NorgTheme
  ) {
    run.foregroundColor = theme.linkColor
    run.underlineStyle = .single
    run.link = url(for: link)
  }

  private static func url(for link: InlineLink) -> URL? {
    if link.kind == .link, let target = link.target, let external = externalURL(for: target) {
      return external
    }
    return NorgLinkURL.encode(link)
  }

  private static func externalURL(for target: String) -> URL? {
    let lower = target.lowercased()
    guard lower.hasPrefix("http://") || lower.hasPrefix("https://") || lower.hasPrefix("file://")
    else {
      return nil
    }
    return URL(string: target)
  }
}