aboutsummaryrefslogtreecommitdiff
path: root/Sources/NorgUI/Math/NorgInlineText.swift
blob: 0d607f58fde6e342f1e1385cb96d4931f034dfc2 (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
import NorgKit
import SwiftUI

/// Renders a run of inline spans as a single `Text`, splicing in rendered
/// images for `.math` spans when a ``NorgMathRenderer`` is configured.
struct NorgInlineText: View {
  @Environment(\.norgTheme) private var theme

  let spans: [InlineSpan]
  var baseFont: Font = .body

  @State private var images: [Int: NorgMathImage] = [:]

  var body: some View {
    text
      .task(id: renderKey) { await renderMath() }
  }

  private var text: Text {
    var result = Text(verbatim: "")
    for (index, span) in spans.enumerated() {
      if span.styles.contains(.math), let image = images[index] {
        result = result + Text(image.swiftUIImage).baselineOffset(-image.descent)
      } else {
        result = result + Text(AttributedString(norg: [span], baseFont: baseFont, theme: theme))
      }
    }
    return result
  }

  private var mathSpans: [(offset: Int, element: InlineSpan)] {
    spans.enumerated().filter { $0.element.styles.contains(.math) }.map { ($0.offset, $0.element) }
  }

  private var renderKey: String {
    mathSpans.map { "\($0.offset):\($0.element.text)" }.joined(separator: "|")
      + "@\(theme.mathFontSize)"
  }

  @MainActor
  private func renderMath() async {
    guard let renderer = theme.mathRenderer, !mathSpans.isEmpty else { return }
    for (index, span) in mathSpans where images[index] == nil {
      if let image = await renderer.render(
        latex: span.text, mode: .inline, fontSize: theme.mathFontSize, color: nil)
      {
        images[index] = image
      }
      await Task.yield()
    }
  }
}