gitbay/Views/Repos/SyntaxHighlighter.swift
87 lines · 3325 bytes
1import Foundation
2import Highlightr
3import SwiftUI
4import UIKit
5
6/// Adapted from hutch: Highlightr wraps a JavaScriptCore context and is
7/// not thread-safe, so an instance stays where it was made. A `nil` result
8/// means "show plain text", never an error.
9nonisolated final class SyntaxHighlighter {
10
11 private let highlightr: Highlightr?
12 private let supported: Set<String>
13
14 init(colorScheme: ColorScheme) {
15 let engine = Highlightr()
16 // Xcode-like in light, muted in dark, so code reads as native.
17 engine?.setTheme(to: colorScheme == .dark ? "atom-one-dark" : "xcode")
18 highlightr = engine
19 supported = Set(engine?.supportedLanguages() ?? [])
20 }
21
22 func highlight(_ code: String, fileName: String, font: UIFont) -> NSAttributedString? {
23 guard let highlightr, let language = Self.language(for: fileName),
24 supported.contains(language) else {
25 return nil
26 }
27 highlightr.theme.setCodeFont(font)
28 return highlightr.highlight(code, as: language, fastRender: true)
29 }
30
31 /// File extension → highlight.js language name. Unknown is fine; the
32 /// caller falls back to plain text.
33 static func language(for fileName: String) -> String? {
34 let name = fileName.lowercased()
35 if name == "makefile" { return "makefile" }
36 if name == "dockerfile" { return "dockerfile" }
37 guard let ext = name.split(separator: ".").last, ext != name else { return nil }
38 switch ext {
39 case "swift": return "swift"
40 case "go": return "go"
41 case "rs": return "rust"
42 case "py": return "python"
43 case "rb": return "ruby"
44 case "js", "mjs", "cjs": return "javascript"
45 case "ts", "tsx": return "typescript"
46 case "jsx": return "javascript"
47 case "c", "h": return "c"
48 case "cc", "cpp", "cxx", "hpp": return "cpp"
49 case "m": return "objectivec"
50 case "cs": return "csharp"
51 case "java": return "java"
52 case "kt", "kts": return "kotlin"
53 case "sh", "bash", "zsh": return "bash"
54 case "fish": return "shell"
55 case "php": return "php"
56 case "pl": return "perl"
57 case "lua": return "lua"
58 case "html", "htm": return "xml"
59 case "xml", "plist", "storyboard", "xib": return "xml"
60 case "css": return "css"
61 case "scss", "sass": return "scss"
62 case "json": return "json"
63 case "yml", "yaml": return "yaml"
64 case "toml", "ini", "cfg": return "ini"
65 case "md", "markdown": return "markdown"
66 case "org": return "plaintext"
67 case "sql": return "sql"
68 case "diff", "patch": return "diff"
69 case "proto": return "protobuf"
70 case "cmake": return "cmake"
71 case "gradle": return "gradle"
72 case "ex", "exs": return "elixir"
73 case "erl": return "erlang"
74 case "hs": return "haskell"
75 case "ml", "mli": return "ocaml"
76 case "clj", "cljs": return "clojure"
77 case "scala": return "scala"
78 case "r": return "r"
79 case "dart": return "dart"
80 case "zig": return "plaintext"
81 case "vim": return "vim"
82 case "tex": return "latex"
83 case "txt", "text", "log": return nil
84 default: return nil
85 }
86 }
87}