Skip to main content

meta_language/
language_catalog.rs

1//! The registered language catalog shared with the JavaScript runtime.
2//!
3//! `src/data/language-catalog.json` is generated from
4//! `parity/language-grammar-inventory.json` by
5//! `js/scripts/build-language-catalog.mjs`; the npm package ships the same
6//! file, so both runtimes agree on every alias, file extension, and default
7//! grammar.
8
9use 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/// A grammar that parses a registered language by default.
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct GrammarProvenance {
19    /// Grammar id in the grammar lock, such as `javascript` or `markdown_inline`,
20    /// or a native grammar id of the catalog, such as `native-json`.
21    pub id: String,
22    /// Exact grammar version (crate version or pinned upstream revision).
23    pub version: String,
24    /// SHA-256 of the generated `parser.c` both runtimes compile, or of a
25    /// native grammar's Links Notation text.
26    pub parser_sha256: String,
27}
28
29/// One registered language.
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct LanguageEntry {
32    /// Canonical inventory name, such as `JavaScript` or `sql-postgres`.
33    pub name: String,
34    /// Inventory family, such as `programming`, `sql`, or `natural`.
35    pub family: String,
36    /// Case-insensitive aliases accepted wherever a language name is.
37    pub aliases: Vec<String>,
38    /// File extensions (lowercase, with the leading dot) that dispatch here.
39    pub extensions: Vec<String>,
40    /// Grammars that parse the language by default, primary grammar first.
41    pub grammars: Vec<GrammarProvenance>,
42}
43
44/// Returns every registered language in inventory order.
45#[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/// Returns the catalog entry for a language name or alias (case-insensitive).
52#[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/// Returns the canonical inventory name for a language name or alias.
64#[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/// Returns every language registered for a file path, most specific first:
70/// languages whose extension is a longer case-insensitive suffix of the path
71/// come before shorter ones, and ties keep catalog order.
72#[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/// Returns the language a file path dispatches to.
93#[must_use]
94pub fn language_for_path(path: &str) -> Option<&'static str> {
95    language_candidates_for_path(path).into_iter().next()
96}
97
98/// Returns the grammars that parse a language by default.
99#[must_use]
100pub fn grammar_provenance(language: &str) -> &'static [GrammarProvenance] {
101    language_entry(language).map_or(&[], |entry| entry.grammars.as_slice())
102}
103
104/// Returns the pinned tree-sitter grammars a language's native default
105/// grammar replaced, which stay its oracles; empty for a language without a
106/// native default grammar.
107#[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/// A native Links Notation grammar of the catalog, which parses its languages
134/// by default, with the kinds its tree places as its tree-sitter oracle does.
135#[derive(Debug, Clone, PartialEq, Eq)]
136pub struct NativeGrammarEntry {
137    /// Grammar id, such as `native-json`.
138    pub id: String,
139    /// The grammar file below `src/data/`, such as `native-grammars/json.lino`.
140    pub file: String,
141    /// Leaf kinds the oracle drops, such as a byte order mark.
142    pub hidden: Vec<String>,
143    /// Leaf kinds the oracle keeps inside a node without a node of their own.
144    pub anonymous: Vec<String>,
145    /// Node kinds the oracle marks as extras.
146    pub extras: Vec<String>,
147    /// The tree-sitter kind of each rule renamed from it, by rule name.
148    pub oracle_kinds: BTreeMap<String, String>,
149}
150
151/// Returns every native grammar of the catalog.
152#[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/// Returns the native grammar of the catalog with id `id`.
184#[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}