lib/syn/src/language.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 //! Forty languages the package can mark up, and mappings from a file path or a short name to one of
2 //! them. A caller often knows a language only from a file path or from a short name such as `py` or
3 //! `bash`, and one language often goes by more than one name.
4 //!
5 //! Matching ignores letter case and surrounding blank space, so `Python`, `PY ` and `py` all name
6 //! Python. Some files carry no extension, such as `Makefile`, `Dockerfile` or `.bashrc`, so their
7 //! whole name has to decide.
8 //!
9 //! - *Chiclet*: this repository's own Lisp-shaped programming language
10 const std = @import("std");
11
12 /// A caller passes one tag to `highlight` or `highlightLine` to choose the scanner for a line. The
13 /// enum has one tag per language the scanners know, forty in all. Each tag is named after its
14 /// language, and `name` gives its short text form. A tag can stand for a family: `shell` covers sh,
15 /// bash, zsh and fish, and `lisp` covers Scheme, Racket, Common Lisp and Emacs Lisp. Tags share
16 /// scanners: `lisp`, `clojure` and `chiclet` use the Lisp scanner, and every language without a
17 /// scanner of its own uses a generic scanner for C-like syntax.
18 pub const Language = enum {
19 zig,
20 /// This repository's own programming language, which `lib/chic` parses and compiles. Its lines
21 /// go through the same scanner as Lisp and Clojure. `fromName` accepts `chiclet` and `chic`,
22 /// and `fromPath` accepts the `.chic` extension and a file named `.chic`.
23 chiclet,
24 lisp,
25 json,
26 shell,
27 diff,
28 markdown,
29 python,
30 yaml,
31 toml,
32 c,
33 cpp,
34 csharp,
35 java,
36 javascript,
37 typescript,
38 go,
39 rust,
40 ruby,
41 php,
42 swift,
43 kotlin,
44 scala,
45 sql,
46 html,
47 css,
48 xml,
49 nix,
50 lua,
51 haskell,
52 elixir,
53 erlang,
54 clojure,
55 ocaml,
56 r,
57 make,
58 perl,
59 dart,
60 vue,
61 svelte,
62 };
63
64 /// A caller that shows or stores a language uses this short name, and `fromName` reads it back. The
65 /// call returns a short lowercase name for the tag. The name equals the tag's own name, except that
66 /// `shell` gives `sh`. `fromName` maps every returned name back to the same tag. The returned text
67 /// is static and needs no freeing.
68 pub fn name(language: Language) []const u8 {
69 return switch (language) {
70 .zig => "zig",
71 .chiclet => "chiclet",
72 .lisp => "lisp",
73 .json => "json",
74 .shell => "sh",
75 .diff => "diff",
76 .markdown => "markdown",
77 .python => "python",
78 .yaml => "yaml",
79 .toml => "toml",
80 .c => "c",
81 .cpp => "cpp",
82 .csharp => "csharp",
83 .java => "java",
84 .javascript => "javascript",
85 .typescript => "typescript",
86 .go => "go",
87 .rust => "rust",
88 .ruby => "ruby",
89 .php => "php",
90 .swift => "swift",
91 .kotlin => "kotlin",
92 .scala => "scala",
93 .sql => "sql",
94 .html => "html",
95 .css => "css",
96 .xml => "xml",
97 .nix => "nix",
98 .lua => "lua",
99 .haskell => "haskell",
100 .elixir => "elixir",
101 .erlang => "erlang",
102 .clojure => "clojure",
103 .ocaml => "ocaml",
104 .r => "r",
105 .make => "make",
106 .perl => "perl",
107 .dart => "dart",
108 .vue => "vue",
109 .svelte => "svelte",
110 };
111 }
112
113 /// A caller turns a short name, such as `zig` in the README example or `bash`, into a tag. The call
114 /// returns the tag whose name or alias matches `raw`, or null when none does. Matching ignores
115 /// letter case, and trims spaces, tabs, carriage returns and newlines from both ends. An empty or
116 /// blank `raw` gives null. The lookup accepts common aliases, for example `zon` for Zig, `js`,
117 /// `jsx` and `node` for JavaScript, `c++` and `cc` for C++, `c#` for C#, `yml` for YAML, and
118 /// `makefile` for Make.
119 pub fn fromName(raw: []const u8) ?Language {
120 const value = std.mem.trim(u8, raw, " \t\r\n");
121 if (value.len == 0) return null;
122 if (eq(value, "zig") or eq(value, "zon")) return .zig;
123 if (eq(value, "chiclet") or eq(value, "chic")) return .chiclet;
124 if (eq(value, "lisp") or eq(value, "scheme") or eq(value, "racket") or eq(value, "common-lisp") or eq(value, "elisp")) return .lisp;
125 if (eq(value, "json") or eq(value, "jsonl")) return .json;
126 if (eq(value, "sh") or eq(value, "shell") or eq(value, "bash") or eq(value, "zsh") or eq(value, "fish")) return .shell;
127 if (eq(value, "diff") or eq(value, "patch")) return .diff;
128 if (eq(value, "markdown") or eq(value, "md") or eq(value, "mdx")) return .markdown;
129 if (eq(value, "python") or eq(value, "py")) return .python;
130 if (eq(value, "yaml") or eq(value, "yml")) return .yaml;
131 if (eq(value, "toml")) return .toml;
132 if (eq(value, "c")) return .c;
133 if (eq(value, "cpp") or eq(value, "c++") or eq(value, "cxx") or eq(value, "cc")) return .cpp;
134 if (eq(value, "csharp") or eq(value, "c#") or eq(value, "cs")) return .csharp;
135 if (eq(value, "java")) return .java;
136 if (eq(value, "javascript") or eq(value, "js") or eq(value, "jsx") or eq(value, "node")) return .javascript;
137 if (eq(value, "typescript") or eq(value, "ts") or eq(value, "tsx")) return .typescript;
138 if (eq(value, "go") or eq(value, "golang")) return .go;
139 if (eq(value, "rust") or eq(value, "rs")) return .rust;
140 if (eq(value, "ruby") or eq(value, "rb")) return .ruby;
141 if (eq(value, "php")) return .php;
142 if (eq(value, "swift")) return .swift;
143 if (eq(value, "kotlin") or eq(value, "kt") or eq(value, "kts")) return .kotlin;
144 if (eq(value, "scala")) return .scala;
145 if (eq(value, "sql") or eq(value, "mysql") or eq(value, "postgres") or eq(value, "postgresql") or eq(value, "sqlite")) return .sql;
146 if (eq(value, "html") or eq(value, "htm")) return .html;
147 if (eq(value, "css") or eq(value, "scss") or eq(value, "sass") or eq(value, "less")) return .css;
148 if (eq(value, "xml") or eq(value, "svg")) return .xml;
149 if (eq(value, "nix")) return .nix;
150 if (eq(value, "lua")) return .lua;
151 if (eq(value, "haskell") or eq(value, "hs")) return .haskell;
152 if (eq(value, "elixir") or eq(value, "ex") or eq(value, "exs")) return .elixir;
153 if (eq(value, "erlang") or eq(value, "erl")) return .erlang;
154 if (eq(value, "clojure") or eq(value, "clj") or eq(value, "cljs") or eq(value, "cljc") or eq(value, "edn")) return .clojure;
155 if (eq(value, "ocaml") or eq(value, "ml") or eq(value, "mli")) return .ocaml;
156 if (eq(value, "r")) return .r;
157 if (eq(value, "make") or eq(value, "makefile")) return .make;
158 if (eq(value, "perl") or eq(value, "pl") or eq(value, "pm")) return .perl;
159 if (eq(value, "dart")) return .dart;
160 if (eq(value, "vue")) return .vue;
161 if (eq(value, "svelte")) return .svelte;
162 return null;
163 }
164
165 /// A caller that opens a file picks the language from its path. The call returns the language for a
166 /// file path, or null when neither its extension nor its file name is known. The lookup tries the
167 /// extension first, ignoring letter case, so `src/main.rs` gives Rust and `src/app.tsx` gives
168 /// TypeScript. A file named `.chic` counts as Chiclet. Without a known extension the lookup tries
169 /// the whole file name through `fromName`, so a file called `Makefile` gives Make. It then knows a
170 /// few names with no extension: `Dockerfile` as shell, `GNUmakefile` and `BSDmakefile` as Make, and
171 /// shell start-up files such as `.bashrc`, `.zshrc` and `.profile` as shell. The lookup reads only
172 /// the path text and never opens the file.
173 pub fn fromPath(path: []const u8) ?Language {
174 const extension = std.fs.path.extension(path);
175 const basename = std.fs.path.basename(path);
176 if (extension.len > 1) {
177 if (fromExtension(extension[1..])) |language| return language;
178 }
179 if (eq(basename, ".chic")) return .chiclet;
180 if (fromName(basename)) |language| return language;
181 if (eq(basename, "Dockerfile")) return .shell;
182 if (eq(basename, "Makefile") or eq(basename, "GNUmakefile") or eq(basename, "BSDmakefile")) return .make;
183 if (eq(basename, ".bashrc") or eq(basename, ".zshrc") or eq(basename, ".profile") or eq(basename, ".bash_profile") or eq(basename, ".zprofile")) return .shell;
184 if (eq(basename, "flake.nix")) return .nix;
185 return null;
186 }
187
188 fn fromExtension(extension: []const u8) ?Language {
189 if (eq(extension, "zig") or eq(extension, "zon")) return .zig;
190 if (eq(extension, "chic")) return .chiclet;
191 if (eq(extension, "lisp") or eq(extension, "lsp") or eq(extension, "scm") or eq(extension, "rkt") or eq(extension, "el")) return .lisp;
192 if (eq(extension, "json") or eq(extension, "jsonl")) return .json;
193 if (eq(extension, "sh") or eq(extension, "bash") or eq(extension, "zsh") or eq(extension, "fish")) return .shell;
194 if (eq(extension, "diff") or eq(extension, "patch")) return .diff;
195 if (eq(extension, "md") or eq(extension, "markdown") or eq(extension, "mdx")) return .markdown;
196 if (eq(extension, "py") or eq(extension, "pyw")) return .python;
197 if (eq(extension, "yaml") or eq(extension, "yml")) return .yaml;
198 if (eq(extension, "toml")) return .toml;
199 if (eq(extension, "c") or eq(extension, "h")) return .c;
200 if (eq(extension, "cc") or eq(extension, "cpp") or eq(extension, "cxx") or eq(extension, "hpp") or eq(extension, "hh") or eq(extension, "hxx")) return .cpp;
201 if (eq(extension, "cs")) return .csharp;
202 if (eq(extension, "java")) return .java;
203 if (eq(extension, "js") or eq(extension, "mjs") or eq(extension, "cjs") or eq(extension, "jsx")) return .javascript;
204 if (eq(extension, "ts") or eq(extension, "mts") or eq(extension, "cts") or eq(extension, "tsx")) return .typescript;
205 if (eq(extension, "go")) return .go;
206 if (eq(extension, "rs")) return .rust;
207 if (eq(extension, "rb") or eq(extension, "rake") or eq(extension, "gemspec")) return .ruby;
208 if (eq(extension, "php") or eq(extension, "phtml")) return .php;
209 if (eq(extension, "swift")) return .swift;
210 if (eq(extension, "kt") or eq(extension, "kts")) return .kotlin;
211 if (eq(extension, "scala") or eq(extension, "sc")) return .scala;
212 if (eq(extension, "sql")) return .sql;
213 if (eq(extension, "html") or eq(extension, "htm")) return .html;
214 if (eq(extension, "css") or eq(extension, "scss") or eq(extension, "sass") or eq(extension, "less")) return .css;
215 if (eq(extension, "xml") or eq(extension, "svg") or eq(extension, "xhtml")) return .xml;
216 if (eq(extension, "nix")) return .nix;
217 if (eq(extension, "lua")) return .lua;
218 if (eq(extension, "hs") or eq(extension, "lhs")) return .haskell;
219 if (eq(extension, "ex") or eq(extension, "exs")) return .elixir;
220 if (eq(extension, "erl") or eq(extension, "hrl")) return .erlang;
221 if (eq(extension, "clj") or eq(extension, "cljs") or eq(extension, "cljc") or eq(extension, "edn")) return .clojure;
222 if (eq(extension, "ml") or eq(extension, "mli")) return .ocaml;
223 if (eq(extension, "r") or eq(extension, "R")) return .r;
224 if (eq(extension, "mk") or eq(extension, "mak")) return .make;
225 if (eq(extension, "pl") or eq(extension, "pm") or eq(extension, "t")) return .perl;
226 if (eq(extension, "dart")) return .dart;
227 if (eq(extension, "vue")) return .vue;
228 if (eq(extension, "svelte")) return .svelte;
229 return null;
230 }
231
232 fn eq(a: []const u8, b: []const u8) bool {
233 return std.ascii.eqlIgnoreCase(a, b);
234 }
235
236 test "language names and paths cover common source files" {
237 try std.testing.expectEqual(Language.zig, fromName("zig").?);
238 try std.testing.expectEqual(Language.shell, fromName("bash").?);
239 try std.testing.expectEqual(Language.javascript, fromName("jsx").?);
240 try std.testing.expectEqual(Language.typescript, fromName("tsx").?);
241 try std.testing.expectEqual(Language.cpp, fromName("c++").?);
242 try std.testing.expectEqual(Language.csharp, fromName("c#").?);
243 try std.testing.expectEqual(Language.rust, fromPath("src/main.rs").?);
244 try std.testing.expectEqual(Language.typescript, fromPath("src/app.tsx").?);
245 try std.testing.expectEqual(Language.chiclet, fromName("chiclet").?);
246 try std.testing.expectEqual(Language.lisp, fromName("lisp").?);
247 try std.testing.expectEqual(Language.chiclet, fromPath("src/program.chic").?);
248 try std.testing.expectEqual(Language.chiclet, fromPath(".chic").?);
249 try std.testing.expectEqual(Language.shell, fromPath("/home/me/.zshrc").?);
250 try std.testing.expectEqualStrings("sh", name(.shell));
251 }