1/**
2 * The languages a composer fence can be switched to from its header, and how
3 * a choice is written back into the fence's info string.
4 *
5 * Ids are the names the highlighter and the file icons both know. The list
6 * is curated rather than every grammar the highlighter bundles: a picker is
7 * for the common case, and any other language can still be typed on the
8 * fence line, where it shows as its own entry.
9 */
10export const CODE_BLOCK_LANGUAGES = [
11 { id: "", label: "Plain text" },
12 { id: "bash", label: "Bash" },
13 { id: "c", label: "C" },
14 { id: "cpp", label: "C++" },
15 { id: "csharp", label: "C#" },
16 { id: "css", label: "CSS" },
17 { id: "dart", label: "Dart" },
18 { id: "diff", label: "Diff" },
19 { id: "dockerfile", label: "Dockerfile" },
20 { id: "elixir", label: "Elixir" },
21 { id: "go", label: "Go" },
22 { id: "graphql", label: "GraphQL" },
23 { id: "haskell", label: "Haskell" },
24 { id: "html", label: "HTML" },
25 { id: "java", label: "Java" },
26 { id: "javascript", label: "JavaScript" },
27 { id: "json", label: "JSON" },
28 { id: "jsx", label: "JSX" },
29 { id: "kotlin", label: "Kotlin" },
30 { id: "lua", label: "Lua" },
31 { id: "markdown", label: "Markdown" },
32 { id: "php", label: "PHP" },
33 { id: "powershell", label: "PowerShell" },
34 { id: "python", label: "Python" },
35 { id: "r", label: "R" },
36 { id: "ruby", label: "Ruby" },
37 { id: "rust", label: "Rust" },
38 { id: "scala", label: "Scala" },
39 { id: "scss", label: "SCSS" },
40 { id: "sql", label: "SQL" },
41 { id: "svelte", label: "Svelte" },
42 { id: "swift", label: "Swift" },
43 { id: "toml", label: "TOML" },
44 { id: "tsx", label: "TSX" },
45 { id: "typescript", label: "TypeScript" },
46 { id: "vue", label: "Vue" },
47 { id: "xml", label: "XML" },
48 { id: "yaml", label: "YAML" },
49 { id: "zig", label: "Zig" },
50] as const;
51
52export type CodeBlockLanguage = { readonly id: string; readonly label: string };
53
54/** Short names people type on the fence line, labelled like the entry they mean. */
55const LANGUAGE_ALIASES: Record<string, string> = {
56 cs: "csharp",
57 "c++": "cpp",
58 js: "javascript",
59 md: "markdown",
60 py: "python",
61 rb: "ruby",
62 rs: "rust",
63 sh: "bash",
64 shell: "bash",
65 ts: "typescript",
66 yml: "yaml",
67};
68
69/** The language is the info string's first word; the rest belongs to the fence. */
70export function languageOfInfoString(info: string): string {
71 return info.trim().split(/\s+/, 1)[0] ?? "";
72}
73
74/**
75 * Replaces the language and keeps everything around it: the whitespace the
76 * fence had before it and whatever follows it. Plain text clears it, unless
77 * something follows: a bare `title=x` would read as the language, so that
78 * keeps `text` in front.
79 */
80export function withInfoStringLanguage(info: string, language: string): string {
81 const leading = /^\s*/.exec(info)![0];
82 // The language is the first word, so what follows it is empty or starts
83 // with whitespace.
84 const rest = info.slice(leading.length + languageOfInfoString(info).length);
85 if (language) return `${leading}${language}${rest}`;
86 return rest.trim() ? `${leading}text${rest}` : "";
87}
88
89/** The canonical entry a language names, if the picker lists it. */
90export function codeLanguageEntry(language: string): CodeBlockLanguage | null {
91 const id = language === "text" ? "" : (LANGUAGE_ALIASES[language.toLowerCase()] ?? language);
92 return CODE_BLOCK_LANGUAGES.find((entry) => entry.id === id.toLowerCase()) ?? null;
93}
94
95export function codeLanguageLabel(language: string): string {
96 return codeLanguageEntry(language)?.label ?? language;
97}