1pub fn language_from_extension(extension: &str) -> &str {
7 match extension.to_lowercase().as_str() {
8 "rs" => "rust",
9 "py" => "python",
10 "js" => "javascript",
11 "ts" => "typescript",
12 "jsx" => "jsx",
13 "tsx" => "tsx",
14 "rb" => "ruby",
15 "go" => "go",
16 "java" => "java",
17 "kt" | "kts" => "kotlin",
18 "c" | "h" => "c",
19 "cpp" | "cc" | "cxx" | "hpp" | "hxx" => "cpp",
20 "cs" => "csharp",
21 "swift" => "swift",
22 "php" => "php",
23 "scala" => "scala",
24 "sh" | "bash" | "zsh" | "fish" => "bash",
25 "ps1" => "powershell",
26 "html" | "htm" => "html",
27 "css" => "css",
28 "scss" => "scss",
29 "sass" => "sass",
30 "less" => "less",
31 "json" => "json",
32 "yaml" | "yml" => "yaml",
33 "toml" => "toml",
34 "xml" => "xml",
35 "sql" => "sql",
36 "md" | "markdown" => "markdown",
37 "dockerfile" => "dockerfile",
38 "lua" => "lua",
39 "r" => "r",
40 "dart" => "dart",
41 "zig" => "zig",
42 "nim" => "nim",
43 "ex" | "exs" => "elixir",
44 "erl" | "hrl" => "erlang",
45 "hs" => "haskell",
46 "ml" | "mli" => "ocaml",
47 "clj" | "cljs" => "clojure",
48 "tf" => "hcl",
49 "vue" => "vue",
50 "svelte" => "svelte",
51 "graphql" | "gql" => "graphql",
52 "proto" => "protobuf",
53 "makefile" | "mk" => "makefile",
54 _ => extension,
55 }
56}
57
58pub fn language_for_path(path: &str) -> &str {
63 let filename = path.rsplit('/').next().unwrap_or(path);
64 match filename.rsplit_once('.') {
65 Some((_, ext)) => language_from_extension(ext),
66 None => language_from_extension(filename),
67 }
68}
69
70pub fn defuse_code_fences(text: &str) -> String {
79 let mut out = String::with_capacity(text.len());
80 let mut backticks = 0;
81 for c in text.chars() {
82 if c == '`' {
83 if backticks == 2 {
84 out.push('\u{200b}');
85 backticks = 0;
86 }
87 backticks += 1;
88 } else {
89 backticks = 0;
90 }
91 out.push(c);
92 }
93 out
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99
100 #[test]
101 fn known_extensions() {
102 assert_eq!(language_from_extension("rs"), "rust");
103 assert_eq!(language_from_extension("py"), "python");
104 assert_eq!(language_from_extension("js"), "javascript");
105 assert_eq!(language_from_extension("ts"), "typescript");
106 assert_eq!(language_from_extension("go"), "go");
107 assert_eq!(language_from_extension("java"), "java");
108 assert_eq!(language_from_extension("cpp"), "cpp");
109 assert_eq!(language_from_extension("cc"), "cpp");
110 assert_eq!(language_from_extension("c"), "c");
111 assert_eq!(language_from_extension("h"), "c");
112 assert_eq!(language_from_extension("hpp"), "cpp");
113 assert_eq!(language_from_extension("cs"), "csharp");
114 assert_eq!(language_from_extension("rb"), "ruby");
115 assert_eq!(language_from_extension("kt"), "kotlin");
116 assert_eq!(language_from_extension("kts"), "kotlin");
117 assert_eq!(language_from_extension("sh"), "bash");
118 assert_eq!(language_from_extension("bash"), "bash");
119 assert_eq!(language_from_extension("zsh"), "bash");
120 assert_eq!(language_from_extension("fish"), "bash");
121 assert_eq!(language_from_extension("yaml"), "yaml");
122 assert_eq!(language_from_extension("yml"), "yaml");
123 assert_eq!(language_from_extension("json"), "json");
124 assert_eq!(language_from_extension("toml"), "toml");
125 assert_eq!(language_from_extension("md"), "markdown");
126 assert_eq!(language_from_extension("ex"), "elixir");
127 assert_eq!(language_from_extension("exs"), "elixir");
128 assert_eq!(language_from_extension("hs"), "haskell");
129 assert_eq!(language_from_extension("tf"), "hcl");
130 assert_eq!(language_from_extension("vue"), "vue");
131 assert_eq!(language_from_extension("svelte"), "svelte");
132 assert_eq!(language_from_extension("proto"), "protobuf");
133 assert_eq!(language_from_extension("graphql"), "graphql");
134 assert_eq!(language_from_extension("gql"), "graphql");
135 }
136
137 #[test]
138 fn case_insensitive() {
139 assert_eq!(language_from_extension("RS"), "rust");
140 assert_eq!(language_from_extension("Py"), "python");
141 assert_eq!(language_from_extension("JS"), "javascript");
142 }
143
144 #[test]
145 fn unknown_extension_returns_as_is() {
146 assert_eq!(language_from_extension("xyz"), "xyz");
147 assert_eq!(language_from_extension("foo"), "foo");
148 }
149
150 #[test]
151 fn extensionless_filenames() {
152 assert_eq!(language_from_extension("Dockerfile"), "dockerfile");
153 assert_eq!(language_from_extension("dockerfile"), "dockerfile");
154 assert_eq!(language_from_extension("Makefile"), "makefile");
155 assert_eq!(language_from_extension("makefile"), "makefile");
156 }
157
158 #[test]
159 fn language_for_path_basic_extension() {
160 assert_eq!(language_for_path("src/main.rs"), "rust");
161 }
162
163 #[test]
164 fn language_for_path_dockerfile_in_subdir() {
165 assert_eq!(language_for_path("docker/Dockerfile"), "dockerfile");
166 }
167
168 #[test]
169 fn language_for_path_dotted_directory() {
170 assert_eq!(language_for_path("some.config/Dockerfile"), "dockerfile");
171 }
172
173 #[test]
174 fn language_for_path_makefile_in_subdir() {
175 assert_eq!(language_for_path("build/Makefile"), "makefile");
176 }
177
178 #[test]
179 fn language_for_path_multiple_dots() {
180 assert_eq!(language_for_path("file.test.ts"), "typescript");
181 }
182
183 #[test]
184 fn language_for_path_dotfile() {
185 assert_eq!(language_for_path(".gitignore"), "gitignore");
186 }
187
188 fn longest_backtick_run(text: &str) -> usize {
190 text.chars()
191 .fold((0, 0), |(longest, run), c| {
192 let run = if c == '`' { run + 1 } else { 0 };
193 (longest.max(run), run)
194 })
195 .0
196 }
197
198 #[test]
199 fn code_without_backticks_is_unchanged() {
200 assert_eq!(defuse_code_fences("fn main() {}"), "fn main() {}");
201 }
202
203 #[test]
204 fn short_backtick_runs_are_preserved() {
205 assert_eq!(defuse_code_fences("let s = `a`;"), "let s = `a`;");
207 assert_eq!(defuse_code_fences("``double``"), "``double``");
208 }
209
210 #[test]
211 fn no_backtick_run_survives_at_fence_length() {
212 for run in 3..=8 {
214 let input = "`".repeat(run);
215 assert!(
216 longest_backtick_run(&defuse_code_fences(&input)) < 3,
217 "a run of {run} backticks was not defused"
218 );
219 }
220 }
221
222 #[test]
223 fn defusing_keeps_surrounding_text() {
224 let defused = defuse_code_fences("before```after");
225 assert!(longest_backtick_run(&defused) < 3);
226 assert!(defused.starts_with("before"));
227 assert!(defused.ends_with("after"));
228 assert_eq!(defused.matches('`').count(), 3);
229 }
230}