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 }