use std::borrow::Cow;
use std::path::Path;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct LanguageId(pub Cow<'static, str>);
impl LanguageId {
pub fn new(s: impl Into<Cow<'static, str>>) -> Self {
Self(s.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for LanguageId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl AsRef<str> for LanguageId {
fn as_ref(&self) -> &str {
self.as_str()
}
}
fn lang_from_ext(ext: &str) -> Option<&'static str> {
match ext {
"rs" => Some("rust"),
"py" | "pyw" | "pyi" => Some("python"),
"ts" => Some("typescript"),
"tsx" => Some("typescriptreact"),
"js" | "mjs" | "cjs" => Some("javascript"),
"jsx" => Some("javascriptreact"),
"go" => Some("go"),
"c" | "h" => Some("c"),
"cpp" | "cc" | "cxx" | "hpp" | "hxx" | "h++" => Some("cpp"),
"java" => Some("java"),
"cs" => Some("csharp"),
"rb" => Some("ruby"),
"lua" => Some("lua"),
"zig" => Some("zig"),
"toml" => Some("toml"),
"yaml" | "yml" => Some("yaml"),
"json" | "jsonc" => Some("json"),
"md" | "markdown" => Some("markdown"),
"sh" | "bash" | "zsh" => Some("shellscript"),
"fish" => Some("fish"),
"html" | "htm" => Some("html"),
"css" => Some("css"),
"scss" => Some("scss"),
"sql" => Some("sql"),
"xml" => Some("xml"),
"svelte" => Some("svelte"),
"vue" => Some("vue"),
"kt" | "kts" => Some("kotlin"),
"swift" => Some("swift"),
"r" => Some("r"),
"dart" => Some("dart"),
"ex" | "exs" => Some("elixir"),
"hs" => Some("haskell"),
"ml" | "mli" => Some("ocaml"),
"clj" | "cljs" => Some("clojure"),
"erl" | "hrl" => Some("erlang"),
"nim" => Some("nim"),
"tf" | "tfvars" => Some("terraform"),
"dockerfile" => Some("dockerfile"),
_ => None,
}
}
fn lang_from_mime(mime: &str) -> Option<&'static str> {
let mime = mime.split(';').next().unwrap_or(mime).trim();
match mime {
"text/rust" | "text/x-rust" => Some("rust"),
"text/x-python" | "text/x-python3" | "application/x-python-code" => Some("python"),
"application/json" | "text/json" => Some("json"),
"text/html" => Some("html"),
"text/css" => Some("css"),
"text/javascript" | "application/javascript" | "application/x-javascript" => {
Some("javascript")
}
"text/typescript" | "application/typescript" => Some("typescript"),
"text/markdown" | "text/x-markdown" => Some("markdown"),
"text/x-yaml" | "application/yaml" | "application/x-yaml" => Some("yaml"),
"application/toml" => Some("toml"),
"text/xml" | "application/xml" => Some("xml"),
"text/x-sh" | "application/x-sh" => Some("shellscript"),
"text/x-sql" | "application/sql" => Some("sql"),
_ => None,
}
}
pub fn detect_language(path: &Path) -> Option<LanguageId> {
if let Some(id) = path
.extension()
.and_then(|e| e.to_str())
.map(|e| e.to_ascii_lowercase())
.as_deref()
.and_then(lang_from_ext)
{
return Some(LanguageId(Cow::Borrowed(id)));
}
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
let lower = name.to_ascii_lowercase();
if let Some(id) = lang_from_ext(&lower) {
return Some(LanguageId(Cow::Borrowed(id)));
}
}
let mime = mime_guess::from_path(path).first_raw()?;
let id = lang_from_mime(mime)?;
Some(LanguageId(Cow::Borrowed(id)))
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
fn id(path: &str) -> Option<String> {
detect_language(Path::new(path)).map(|l| l.as_str().to_string())
}
#[test]
fn rust_extension() {
assert_eq!(id("main.rs"), Some("rust".into()));
}
#[test]
fn cpp_extensions() {
assert_eq!(id("foo.cpp"), Some("cpp".into()));
assert_eq!(id("foo.hpp"), Some("cpp".into()));
assert_eq!(id("foo.cc"), Some("cpp".into()));
}
#[test]
fn python_extension() {
assert_eq!(id("script.py"), Some("python".into()));
assert_eq!(id("types.pyi"), Some("python".into()));
}
#[test]
fn toml_extension() {
assert_eq!(id("Cargo.toml"), Some("toml".into()));
}
#[test]
fn yaml_extensions() {
assert_eq!(id("config.yaml"), Some("yaml".into()));
assert_eq!(id("config.yml"), Some("yaml".into()));
}
#[test]
fn unknown_extension_returns_none() {
assert_eq!(id("archive.tar"), None);
assert_eq!(id("image.png"), None);
assert_eq!(id("binary.exe"), None);
assert_eq!(id("no_extension"), None);
}
#[test]
fn case_insensitive_extension() {
assert_eq!(id("main.RS"), Some("rust".into()));
assert_eq!(id("main.Py"), Some("python".into()));
}
#[test]
fn dockerfile_bare_name() {
assert_eq!(id("Dockerfile"), Some("dockerfile".into()));
}
#[test]
fn language_id_display() {
let lang = LanguageId::new("rust");
assert_eq!(lang.to_string(), "rust");
assert_eq!(lang.as_str(), "rust");
}
#[test]
fn language_id_owned() {
let lang = LanguageId::new(String::from("my-lang"));
assert_eq!(lang.as_str(), "my-lang");
}
#[test]
fn language_id_equality() {
assert_eq!(LanguageId::new("rust"), LanguageId::new("rust"));
assert_ne!(LanguageId::new("rust"), LanguageId::new("python"));
}
}