meta_language/
language_catalog.rs1use std::collections::BTreeMap;
10use std::sync::OnceLock;
11
12use serde_json::Value;
13
14const LANGUAGE_CATALOG_JSON: &str = include_str!("data/language-catalog.json");
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct GrammarProvenance {
19 pub id: String,
22 pub version: String,
24 pub parser_sha256: String,
27}
28
29#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct LanguageEntry {
32 pub name: String,
34 pub family: String,
36 pub aliases: Vec<String>,
38 pub extensions: Vec<String>,
40 pub grammars: Vec<GrammarProvenance>,
42}
43
44#[must_use]
46pub fn language_catalog() -> &'static [LanguageEntry] {
47 static CATALOG: OnceLock<Vec<LanguageEntry>> = OnceLock::new();
48 CATALOG.get_or_init(parse_catalog)
49}
50
51#[must_use]
53pub fn language_entry(language: &str) -> Option<&'static LanguageEntry> {
54 language_catalog().iter().find(|entry| {
55 entry.name.eq_ignore_ascii_case(language)
56 || entry
57 .aliases
58 .iter()
59 .any(|alias| alias.eq_ignore_ascii_case(language))
60 })
61}
62
63#[must_use]
65pub fn canonical_language_name(language: &str) -> Option<&'static str> {
66 language_entry(language).map(|entry| entry.name.as_str())
67}
68
69#[must_use]
73pub fn language_candidates_for_path(path: &str) -> Vec<&'static str> {
74 let lower = path.to_lowercase();
75 let mut candidates: Vec<(usize, usize, &'static str)> = language_catalog()
76 .iter()
77 .enumerate()
78 .filter_map(|(index, entry)| {
79 entry
80 .extensions
81 .iter()
82 .filter(|extension| lower.ends_with(extension.as_str()))
83 .map(String::len)
84 .max()
85 .map(|length| (length, index, entry.name.as_str()))
86 })
87 .collect();
88 candidates.sort_by(|left, right| right.0.cmp(&left.0).then(left.1.cmp(&right.1)));
89 candidates.into_iter().map(|(_, _, name)| name).collect()
90}
91
92#[must_use]
94pub fn language_for_path(path: &str) -> Option<&'static str> {
95 language_candidates_for_path(path).into_iter().next()
96}
97
98#[must_use]
100pub fn grammar_provenance(language: &str) -> &'static [GrammarProvenance] {
101 language_entry(language).map_or(&[], |entry| entry.grammars.as_slice())
102}
103
104#[must_use]
108pub fn oracle_grammar_provenance(language: &str) -> &'static [GrammarProvenance] {
109 static ORACLES: OnceLock<Vec<(String, Vec<GrammarProvenance>)>> = OnceLock::new();
110 let Some(entry) = language_entry(language) else {
111 return &[];
112 };
113 ORACLES
114 .get_or_init(|| {
115 catalog_value()["languages"]
116 .as_array()
117 .expect("language catalog lists languages")
118 .iter()
119 .filter(|language| language["oracleGrammars"].is_array())
120 .map(|language| {
121 (
122 string(&language["name"]),
123 provenance(&language["oracleGrammars"]),
124 )
125 })
126 .collect()
127 })
128 .iter()
129 .find(|(name, _)| *name == entry.name)
130 .map_or(&[], |(_, grammars)| grammars.as_slice())
131}
132
133#[derive(Debug, Clone, PartialEq, Eq)]
136pub struct NativeGrammarEntry {
137 pub id: String,
139 pub file: String,
141 pub hidden: Vec<String>,
143 pub anonymous: Vec<String>,
145 pub extras: Vec<String>,
147 pub oracle_kinds: BTreeMap<String, String>,
149}
150
151#[must_use]
153pub fn native_grammars() -> &'static [NativeGrammarEntry] {
154 static NATIVE: OnceLock<Vec<NativeGrammarEntry>> = OnceLock::new();
155 NATIVE.get_or_init(|| {
156 catalog_value()["nativeGrammars"]
157 .as_object()
158 .map(|grammars| {
159 grammars
160 .iter()
161 .map(|(id, grammar)| NativeGrammarEntry {
162 id: id.clone(),
163 file: string(&grammar["file"]),
164 hidden: strings(&grammar["hidden"]),
165 anonymous: strings(&grammar["anonymous"]),
166 extras: strings(&grammar["extras"]),
167 oracle_kinds: grammar["oracleKinds"]
168 .as_object()
169 .map(|kinds| {
170 kinds
171 .iter()
172 .map(|(name, kind)| (name.clone(), string(kind)))
173 .collect()
174 })
175 .unwrap_or_default(),
176 })
177 .collect()
178 })
179 .unwrap_or_default()
180 })
181}
182
183#[must_use]
185pub fn native_grammar(id: &str) -> Option<&'static NativeGrammarEntry> {
186 native_grammars().iter().find(|grammar| grammar.id == id)
187}
188
189fn catalog_value() -> &'static Value {
190 static VALUE: OnceLock<Value> = OnceLock::new();
191 VALUE.get_or_init(|| {
192 serde_json::from_str(LANGUAGE_CATALOG_JSON).expect("language catalog is valid JSON")
193 })
194}
195
196fn provenance(grammars: &Value) -> Vec<GrammarProvenance> {
197 grammars
198 .as_array()
199 .expect("language grammars")
200 .iter()
201 .map(|grammar| GrammarProvenance {
202 id: string(&grammar["id"]),
203 version: string(&grammar["version"]),
204 parser_sha256: string(&grammar["parserSha256"]),
205 })
206 .collect()
207}
208
209fn parse_catalog() -> Vec<LanguageEntry> {
210 catalog_value()["languages"]
211 .as_array()
212 .expect("language catalog lists languages")
213 .iter()
214 .map(|language| LanguageEntry {
215 name: string(&language["name"]),
216 family: string(&language["family"]),
217 aliases: strings(&language["aliases"]),
218 extensions: strings(&language["extensions"]),
219 grammars: provenance(&language["grammars"]),
220 })
221 .collect()
222}
223
224fn string(value: &Value) -> String {
225 value.as_str().expect("language catalog string").to_string()
226}
227
228fn strings(value: &Value) -> Vec<String> {
229 value
230 .as_array()
231 .expect("language catalog array")
232 .iter()
233 .map(string)
234 .collect()
235}