1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5#[non_exhaustive]
6pub enum LanguageHint<'a> {
7 Id(&'a str),
8 InfoString(&'a str),
9 Path(&'a str),
10 Auto,
11}
12
13impl<'a> From<&'a str> for LanguageHint<'a> {
14 fn from(value: &'a str) -> Self {
15 Self::Id(value)
16 }
17}
18
19impl<'a> LanguageHint<'a> {
20 #[must_use]
22 pub fn as_str(&self) -> &'a str {
23 match self {
24 Self::Id(value) | Self::Path(value) => value,
25 Self::InfoString(value) => value.split_ascii_whitespace().next().unwrap_or_default(),
26 Self::Auto => "",
27 }
28 }
29}
30
31pub fn resolve_language<'a>(
33 hint: impl Into<LanguageHint<'a>>,
34 source: &str,
35) -> Option<&'static str> {
36 let hint = hint.into().as_str();
37 let normalized = hint.trim().replace('\\', "/").to_ascii_lowercase();
38 if normalized.is_empty() {
39 return shebang_id(source);
40 }
41
42 let simple = normalized.rsplit('/').next().unwrap_or(&normalized);
43 canonical_id(&normalized)
44 .or_else(|| canonical_id(simple))
45 .or_else(|| extension_id(simple))
46 .or_else(|| special_file(simple))
47 .or_else(|| arborium::detect_language(&normalized).and_then(canonical_id))
48 .or_else(|| shebang_id(source))
49}
50
51fn canonical_id(hint: &str) -> Option<&'static str> {
52 Some(match hint {
53 "rust" | "rs" => "rust",
54 "javascript" | "js" | "mjs" | "cjs" | "node" | "jsx" => "javascript",
55 "typescript" | "ts" | "mts" | "cts" => "typescript",
56 "tsx" => "tsx",
57 "python" | "py" | "python3" => "python",
58 "bash" | "sh" | "shell" => "bash",
59 "zsh" => "zsh",
60 "batch" | "bat" | "cmd" => "batch",
61 "c" | "h" => "c",
62 "csharp" | "c-sharp" | "c#" | "cs" => "c-sharp",
63 "cpp" | "c++" | "cc" | "cxx" | "hpp" | "hh" | "hxx" => "cpp",
64 "go" | "golang" => "go",
65 "java" => "java",
66 "kotlin" | "kt" | "kts" => "kotlin",
67 "ruby" | "rb" => "ruby",
68 "swift" => "swift",
69 "php" => "php",
70 "sql" => "sql",
71 "lua" => "lua",
72 "dockerfile" => "dockerfile",
73 "json" | "jsonc" => "json",
74 "toml" => "toml",
75 "yaml" | "yml" => "yaml",
76 "html" | "htm" => "html",
77 "css" => "css",
78 "markdown" | "md" => "markdown",
79 "zig" => "zig",
80 "nix" => "nix",
81 "haskell" | "hs" => "haskell",
82 "elixir" | "ex" | "exs" => "elixir",
83 "erlang" | "erl" | "hrl" => "erlang",
84 "scala" | "sc" => "scala",
85 "clojure" | "clj" | "cljs" | "cljc" | "edn" => "clojure",
86 "commonlisp" | "common-lisp" | "lisp" | "cl" => "commonlisp",
87 "scheme" | "scm" | "ss" => "scheme",
88 "ocaml" | "ml" | "mli" => "ocaml",
89 "fsharp" | "f#" | "fs" | "fsi" | "fsx" => "fsharp",
90 "dart" => "dart",
91 "powershell" | "pwsh" | "ps1" | "psm1" => "powershell",
92 "fish" => "fish",
93 "make" | "makefile" => "make",
94 "cmake" => "cmake",
95 "ninja" => "ninja",
96 "meson" => "meson",
97 "just" | "justfile" => "just",
98 "hcl" | "terraform" | "tf" | "tfvars" => "hcl",
99 "graphql" | "gql" => "graphql",
100 "protobuf" | "proto" => "proto",
101 "xml" | "xhtml" | "svg" => "xml",
102 "vue" => "vue",
103 "svelte" => "svelte",
104 "scss" => "scss",
105 "asm" | "assembly" => "asm",
106 "x86asm" | "x86-asm" | "nasm" => "x86asm",
107 "objective-c" | "objectivec" | "objc" => "objc",
108 "perl" | "pl" | "pm" => "perl",
109 "r" => "r",
110 "solidity" | "sol" => "solidity",
111 "starlark" | "bzl" | "bazel" => "starlark",
112 "rego" => "rego",
113 "ini" | "cfg" => "ini",
114 "diff" | "patch" => "diff",
115 _ => return None,
116 })
117}
118
119fn extension_id(file: &str) -> Option<&'static str> {
120 let extension = file.rsplit_once('.')?.1;
121 canonical_id(extension).or(match extension {
122 "s" => Some("asm"),
125 "m" | "mm" => Some("objc"),
126 _ => None,
127 })
128}
129
130fn special_file(file: &str) -> Option<&'static str> {
131 match file {
132 "dockerfile" | "containerfile" => Some("dockerfile"),
133 "go.mod" | "go.sum" => Some("go"),
134 "makefile" | "gnumakefile" => Some("make"),
135 "cmakelists.txt" => Some("cmake"),
136 "build.ninja" => Some("ninja"),
137 "meson.build" | "meson_options.txt" => Some("meson"),
138 "justfile" => Some("just"),
139 "flake.nix" => Some("nix"),
140 ".terraformrc" => Some("hcl"),
141 "workspace" => Some("starlark"),
142 "build.sbt" => Some("scala"),
143 "deps.edn" => Some("clojure"),
144 ".zshrc" => Some("zsh"),
145 ".bashrc" => Some("bash"),
146 _ => None,
147 }
148}
149
150fn shebang_id(source: &str) -> Option<&'static str> {
151 let line = source.lines().next()?;
152 if !line.starts_with("#!") {
153 return None;
154 }
155 let lower = line.to_ascii_lowercase();
156 if lower.contains("python") {
157 Some("python")
158 } else if lower.contains("node") {
159 Some("javascript")
160 } else if lower.contains("zsh") {
161 Some("zsh")
162 } else if lower.contains("bash") || lower.contains("/sh") {
163 Some("bash")
164 } else if lower.contains("fish") {
165 Some("fish")
166 } else if lower.contains("pwsh") || lower.contains("powershell") {
167 Some("powershell")
168 } else {
169 None
170 }
171}