Skip to main content

normalize_languages/
typst.rs

1//! Typst language support.
2
3use crate::external_packages::ResolvedPackage;
4use crate::{Export, Import, Language, Symbol, SymbolKind, Visibility, VisibilityMechanism};
5use std::path::{Path, PathBuf};
6use tree_sitter::Node;
7
8/// Typst language support.
9pub struct Typst;
10
11impl Language for Typst {
12    fn name(&self) -> &'static str {
13        "Typst"
14    }
15    fn extensions(&self) -> &'static [&'static str] {
16        &["typ"]
17    }
18    fn grammar_name(&self) -> &'static str {
19        "typst"
20    }
21
22    fn has_symbols(&self) -> bool {
23        true
24    }
25
26    fn container_kinds(&self) -> &'static [&'static str] {
27        &[]
28    }
29
30    fn function_kinds(&self) -> &'static [&'static str] {
31        &["let"]
32    }
33
34    fn type_kinds(&self) -> &'static [&'static str] {
35        &[]
36    }
37
38    fn import_kinds(&self) -> &'static [&'static str] {
39        &["import"]
40    }
41
42    fn public_symbol_kinds(&self) -> &'static [&'static str] {
43        &["let"]
44    }
45
46    fn visibility_mechanism(&self) -> VisibilityMechanism {
47        VisibilityMechanism::AllPublic
48    }
49
50    fn extract_public_symbols(&self, node: &Node, content: &str) -> Vec<Export> {
51        if node.kind() != "let" {
52            return Vec::new();
53        }
54
55        if let Some(name) = self.node_name(node, content) {
56            return vec![Export {
57                name: name.to_string(),
58                kind: SymbolKind::Function,
59                line: node.start_position().row + 1,
60            }];
61        }
62
63        Vec::new()
64    }
65
66    fn scope_creating_kinds(&self) -> &'static [&'static str] {
67        &["let", "block"]
68    }
69
70    fn control_flow_kinds(&self) -> &'static [&'static str] {
71        &["if", "while", "for"]
72    }
73
74    fn complexity_nodes(&self) -> &'static [&'static str] {
75        &["if", "while", "for"]
76    }
77
78    fn nesting_nodes(&self) -> &'static [&'static str] {
79        &["if", "while", "for", "block"]
80    }
81
82    fn signature_suffix(&self) -> &'static str {
83        ""
84    }
85
86    fn extract_function(&self, node: &Node, content: &str, _in_container: bool) -> Option<Symbol> {
87        if node.kind() != "let" {
88            return None;
89        }
90
91        let name = self.node_name(node, content)?;
92        let text = &content[node.byte_range()];
93        let first_line = text.lines().next().unwrap_or(text);
94
95        Some(Symbol {
96            name: name.to_string(),
97            kind: SymbolKind::Function,
98            signature: first_line.trim().to_string(),
99            docstring: None,
100            attributes: Vec::new(),
101            start_line: node.start_position().row + 1,
102            end_line: node.end_position().row + 1,
103            visibility: Visibility::Public,
104            children: Vec::new(),
105            is_interface_impl: false,
106            implements: Vec::new(),
107        })
108    }
109
110    fn extract_container(&self, _node: &Node, _content: &str) -> Option<Symbol> {
111        None
112    }
113    fn extract_type(&self, _node: &Node, _content: &str) -> Option<Symbol> {
114        None
115    }
116    fn extract_docstring(&self, _node: &Node, _content: &str) -> Option<String> {
117        None
118    }
119
120    fn extract_attributes(&self, _node: &Node, _content: &str) -> Vec<String> {
121        Vec::new()
122    }
123
124    fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
125        if node.kind() != "import" {
126            return Vec::new();
127        }
128
129        let text = &content[node.byte_range()];
130        vec![Import {
131            module: text.trim().to_string(),
132            names: Vec::new(),
133            alias: None,
134            is_wildcard: text.contains('*'),
135            is_relative: false,
136            line: node.start_position().row + 1,
137        }]
138    }
139
140    fn format_import(&self, import: &Import, names: Option<&[&str]>) -> String {
141        // Typst: #import "file.typ" or #import "file.typ": item1, item2
142        let names_to_use: Vec<&str> = names
143            .map(|n| n.to_vec())
144            .unwrap_or_else(|| import.names.iter().map(|s| s.as_str()).collect());
145        if names_to_use.is_empty() {
146            format!("#import \"{}\"", import.module)
147        } else if import.is_wildcard {
148            format!("#import \"{}\": *", import.module)
149        } else {
150            format!("#import \"{}\": {}", import.module, names_to_use.join(", "))
151        }
152    }
153
154    fn is_public(&self, _node: &Node, _content: &str) -> bool {
155        true
156    }
157    fn get_visibility(&self, _node: &Node, _content: &str) -> Visibility {
158        Visibility::Public
159    }
160
161    fn is_test_symbol(&self, _symbol: &crate::Symbol) -> bool {
162        false
163    }
164
165    fn embedded_content(&self, _node: &Node, _content: &str) -> Option<crate::EmbeddedBlock> {
166        None
167    }
168
169    fn container_body<'a>(&self, _node: &'a Node<'a>) -> Option<Node<'a>> {
170        None
171    }
172    fn body_has_docstring(&self, _body: &Node, _content: &str) -> bool {
173        false
174    }
175
176    fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
177        node.child_by_field_name("name")
178            .map(|n| &content[n.byte_range()])
179    }
180
181    fn file_path_to_module_name(&self, path: &Path) -> Option<String> {
182        let ext = path.extension()?.to_str()?;
183        if ext != "typ" {
184            return None;
185        }
186        let stem = path.file_stem()?.to_str()?;
187        Some(stem.to_string())
188    }
189
190    fn module_name_to_paths(&self, module: &str) -> Vec<String> {
191        vec![format!("{}.typ", module)]
192    }
193
194    fn lang_key(&self) -> &'static str {
195        "typst"
196    }
197
198    fn is_stdlib_import(&self, _: &str, _: &Path) -> bool {
199        false
200    }
201    fn find_stdlib(&self, _project_root: &Path) -> Option<PathBuf> {
202        None
203    }
204    fn resolve_local_import(&self, _: &str, _: &Path, _: &Path) -> Option<PathBuf> {
205        None
206    }
207    fn resolve_external_import(&self, _: &str, _: &Path) -> Option<ResolvedPackage> {
208        None
209    }
210    fn get_version(&self, _: &Path) -> Option<String> {
211        None
212    }
213    fn find_package_cache(&self, _: &Path) -> Option<PathBuf> {
214        None
215    }
216    fn indexable_extensions(&self) -> &'static [&'static str] {
217        &["typ"]
218    }
219    fn package_sources(&self, _: &Path) -> Vec<crate::PackageSource> {
220        Vec::new()
221    }
222
223    fn should_skip_package_entry(&self, name: &str, is_dir: bool) -> bool {
224        use crate::traits::{has_extension, skip_dotfiles};
225        if skip_dotfiles(name) {
226            return true;
227        }
228        !is_dir && !has_extension(name, self.indexable_extensions())
229    }
230
231    fn discover_packages(&self, _: &crate::PackageSource) -> Vec<(String, PathBuf)> {
232        Vec::new()
233    }
234
235    fn package_module_name(&self, entry_name: &str) -> String {
236        entry_name
237            .strip_suffix(".typ")
238            .unwrap_or(entry_name)
239            .to_string()
240    }
241
242    fn find_package_entry(&self, path: &Path) -> Option<PathBuf> {
243        if path.is_file() {
244            Some(path.to_path_buf())
245        } else {
246            None
247        }
248    }
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254    use crate::validate_unused_kinds_audit;
255
256    #[test]
257    fn unused_node_kinds_audit() {
258        #[rustfmt::skip]
259        let documented_unused: &[&str] = &[
260            // Math mode
261            "formula",
262            // Control flow (not function definitions)
263            "return",
264            // Inline lambdas are not top-level definitions
265            "lambda",
266        ];
267        validate_unused_kinds_audit(&Typst, documented_unused)
268            .expect("Typst unused node kinds audit failed");
269    }
270}