import Foundation import Highlightr import SwiftUI import UIKit /// Adapted from hutch: Highlightr wraps a JavaScriptCore context and is /// not thread-safe, so an instance stays where it was made. A `nil` result /// means "show plain text", never an error. nonisolated final class SyntaxHighlighter { private let highlightr: Highlightr? private let supported: Set init(colorScheme: ColorScheme) { let engine = Highlightr() // Xcode-like in light, muted in dark, so code reads as native. engine?.setTheme(to: colorScheme == .dark ? "atom-one-dark" : "xcode") highlightr = engine supported = Set(engine?.supportedLanguages() ?? []) } func highlight(_ code: String, fileName: String, font: UIFont) -> NSAttributedString? { guard let highlightr, let language = Self.language(for: fileName), supported.contains(language) else { return nil } highlightr.theme.setCodeFont(font) return highlightr.highlight(code, as: language, fastRender: true) } /// File extension → highlight.js language name. Unknown is fine; the /// caller falls back to plain text. static func language(for fileName: String) -> String? { let name = fileName.lowercased() if name == "makefile" { return "makefile" } if name == "dockerfile" { return "dockerfile" } guard let ext = name.split(separator: ".").last, ext != name else { return nil } switch ext { case "swift": return "swift" case "go": return "go" case "rs": return "rust" case "py": return "python" case "rb": return "ruby" case "js", "mjs", "cjs": return "javascript" case "ts", "tsx": return "typescript" case "jsx": return "javascript" case "c", "h": return "c" case "cc", "cpp", "cxx", "hpp": return "cpp" case "m": return "objectivec" case "cs": return "csharp" case "java": return "java" case "kt", "kts": return "kotlin" case "sh", "bash", "zsh": return "bash" case "fish": return "shell" case "php": return "php" case "pl": return "perl" case "lua": return "lua" case "html", "htm": return "xml" case "xml", "plist", "storyboard", "xib": return "xml" case "css": return "css" case "scss", "sass": return "scss" case "json": return "json" case "yml", "yaml": return "yaml" case "toml", "ini", "cfg": return "ini" case "md", "markdown": return "markdown" case "org": return "plaintext" case "sql": return "sql" case "diff", "patch": return "diff" case "proto": return "protobuf" case "cmake": return "cmake" case "gradle": return "gradle" case "ex", "exs": return "elixir" case "erl": return "erlang" case "hs": return "haskell" case "ml", "mli": return "ocaml" case "clj", "cljs": return "clojure" case "scala": return "scala" case "r": return "r" case "dart": return "dart" case "zig": return "plaintext" case "vim": return "vim" case "tex": return "latex" case "txt", "text", "log": return nil default: return nil } } }