Skip to main content

mdbook_treesitter/
language.rs

1//! Language registry: maps file extensions to tree-sitter parsers.
2//!
3//! Queries are intentionally not bundled here — define them in `book.toml`
4//! under `[preprocessor.treesitter.<lang>.queries]` so they can be edited
5//! without recompiling or reinstalling the preprocessor binary.
6
7use anyhow::{Context, Result, bail};
8use std::collections::HashMap;
9use tree_sitter::{Language, Parser};
10
11use crate::config::QueryConfig;
12
13/// A resolved language entry: the tree-sitter `Language` plus any named queries
14/// that are available for it.
15pub struct LanguageEntry {
16    pub language: Language,
17    /// Named queries keyed by query name.
18    pub queries: HashMap<String, QueryConfig>,
19}
20
21/// Builds the file-extension → `LanguageEntry` map, merging the built-in
22/// defaults with any user-supplied overrides from `book.toml`.
23///
24/// `user_configs` maps language name (as used in `book.toml`) to its config.
25/// `book_root` is used to resolve relative parser paths.
26pub fn build_registry(
27    user_configs: &HashMap<String, crate::config::LanguageConfig>,
28    book_root: &std::path::Path,
29) -> Result<HashMap<String, LanguageEntry>> {
30    let mut registry: HashMap<String, LanguageEntry> = HashMap::new();
31
32    // ── Rust ──────────────────────────────────────────────────────────────
33    {
34        registry.insert(
35            "rs".into(),
36            LanguageEntry {
37                language: Language::new(tree_sitter_rust::LANGUAGE),
38                queries: HashMap::new(),
39            },
40        );
41    }
42
43    // ── TOML ─────────────────────────────────────────────────────────────
44    {
45        registry.insert(
46            "toml".into(),
47            LanguageEntry {
48                language: tree_sitter_toml::language(),
49                queries: HashMap::new(),
50            },
51        );
52    }
53
54    // ── Markdown ─────────────────────────────────────────────────────────
55    {
56        registry.insert(
57            "md".into(),
58            LanguageEntry {
59                language: Language::new(tree_sitter_md::LANGUAGE),
60                queries: HashMap::new(),
61            },
62        );
63    }
64
65    // ── User-supplied overrides ───────────────────────────────────────────
66    for (lang_name, lang_cfg) in user_configs {
67        let ext = lang_name_to_ext(lang_name);
68
69        if let Some(parser_path_str) = &lang_cfg.parser {
70            let parser_path = if std::path::Path::new(parser_path_str).is_absolute() {
71                std::path::PathBuf::from(parser_path_str)
72            } else {
73                book_root.join(parser_path_str)
74            };
75            let language = unsafe { load_language_from_so(&parser_path)? };
76            let entry = registry
77                .entry(ext.to_string())
78                .or_insert_with(|| LanguageEntry {
79                    language: language.clone(),
80                    queries: HashMap::new(),
81                });
82            entry.language = language;
83        }
84
85        // Merge user queries on top of any built-ins.
86        if let Some(entry) = registry.get_mut(ext) {
87            for (qname, qcfg) in &lang_cfg.queries {
88                entry.queries.insert(qname.clone(), qcfg.clone());
89            }
90        }
91    }
92
93    Ok(registry)
94}
95
96fn lang_name_to_ext(name: &str) -> &str {
97    match name {
98        "rust" => "rs",
99        "markdown" => "md",
100        "javascript" => "js",
101        "typescript" => "ts",
102        "python" => "py",
103        "c" => "c",
104        "cpp" | "c++" => "cpp",
105        "go" => "go",
106        other => other,
107    }
108}
109
110/// Loads a tree-sitter `Language` from an external shared library.
111///
112/// # Safety
113/// The `.so` must export a valid tree-sitter C ABI symbol.
114unsafe fn load_language_from_so(_path: &std::path::Path) -> Result<Language> {
115    bail!(
116        "Dynamic parser loading from `{}` is not yet supported in this build.",
117        _path.display()
118    )
119}
120
121/// Creates a configured `Parser` for the given language.
122pub fn make_parser(language: &Language) -> Result<Parser> {
123    let mut parser = Parser::new();
124    parser
125        .set_language(language)
126        .context("Failed to configure parser")?;
127    Ok(parser)
128}