Skip to main content

normalize_languages/
asciidoc.rs

1//! AsciiDoc 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/// AsciiDoc language support.
9pub struct AsciiDoc;
10
11impl Language for AsciiDoc {
12    fn name(&self) -> &'static str {
13        "AsciiDoc"
14    }
15    fn extensions(&self) -> &'static [&'static str] {
16        &["adoc", "asciidoc", "asc"]
17    }
18    fn grammar_name(&self) -> &'static str {
19        "asciidoc"
20    }
21
22    fn has_symbols(&self) -> bool {
23        true
24    }
25
26    fn container_kinds(&self) -> &'static [&'static str] {
27        &["section_block"]
28    }
29
30    fn function_kinds(&self) -> &'static [&'static str] {
31        &[]
32    }
33    fn type_kinds(&self) -> &'static [&'static str] {
34        &[]
35    }
36
37    fn import_kinds(&self) -> &'static [&'static str] {
38        &["block_macro"] // includes are block macros in AsciiDoc
39    }
40
41    fn public_symbol_kinds(&self) -> &'static [&'static str] {
42        &[
43            "section_block",
44            "title1",
45            "title2",
46            "title3",
47            "title4",
48            "title5",
49        ]
50    }
51
52    fn visibility_mechanism(&self) -> VisibilityMechanism {
53        VisibilityMechanism::AllPublic
54    }
55
56    fn extract_public_symbols(&self, node: &Node, content: &str) -> Vec<Export> {
57        match node.kind() {
58            "section_block" | "title1" | "title2" | "title3" | "title4" | "title5" => {
59                if let Some(name) = self.node_name(node, content) {
60                    return vec![Export {
61                        name: name.to_string(),
62                        kind: SymbolKind::Module,
63                        line: node.start_position().row + 1,
64                    }];
65                }
66            }
67            _ => {}
68        }
69        Vec::new()
70    }
71
72    fn scope_creating_kinds(&self) -> &'static [&'static str] {
73        &["section_block"]
74    }
75
76    fn control_flow_kinds(&self) -> &'static [&'static str] {
77        &[]
78    }
79    fn complexity_nodes(&self) -> &'static [&'static str] {
80        &[]
81    }
82    fn nesting_nodes(&self) -> &'static [&'static str] {
83        &["section_block"]
84    }
85
86    fn signature_suffix(&self) -> &'static str {
87        ""
88    }
89
90    fn extract_function(
91        &self,
92        _node: &Node,
93        _content: &str,
94        _in_container: bool,
95    ) -> Option<Symbol> {
96        None
97    }
98
99    fn extract_container(&self, node: &Node, content: &str) -> Option<Symbol> {
100        if node.kind() != "section_block" {
101            return None;
102        }
103
104        let name = self.node_name(node, content)?;
105        let text = &content[node.byte_range()];
106        let first_line = text.lines().next().unwrap_or(text);
107
108        Some(Symbol {
109            name: name.to_string(),
110            kind: SymbolKind::Module,
111            signature: first_line.trim().to_string(),
112            docstring: None,
113            attributes: Vec::new(),
114            start_line: node.start_position().row + 1,
115            end_line: node.end_position().row + 1,
116            visibility: Visibility::Public,
117            children: Vec::new(),
118            is_interface_impl: false,
119            implements: Vec::new(),
120        })
121    }
122
123    fn extract_type(&self, _node: &Node, _content: &str) -> Option<Symbol> {
124        None
125    }
126    fn extract_docstring(&self, _node: &Node, _content: &str) -> Option<String> {
127        None
128    }
129
130    fn extract_attributes(&self, _node: &Node, _content: &str) -> Vec<String> {
131        Vec::new()
132    }
133
134    fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
135        if node.kind() != "block_macro" {
136            return Vec::new();
137        }
138
139        let text = &content[node.byte_range()];
140        // Only include macros are imports
141        if !text.starts_with("include::") {
142            return Vec::new();
143        }
144
145        vec![Import {
146            module: text.trim().to_string(),
147            names: Vec::new(),
148            alias: None,
149            is_wildcard: false,
150            is_relative: false,
151            line: node.start_position().row + 1,
152        }]
153    }
154
155    fn format_import(&self, _import: &Import, _names: Option<&[&str]>) -> String {
156        // AsciiDoc has no imports
157        String::new()
158    }
159
160    fn is_public(&self, _node: &Node, _content: &str) -> bool {
161        true
162    }
163    fn get_visibility(&self, _node: &Node, _content: &str) -> Visibility {
164        Visibility::Public
165    }
166
167    fn is_test_symbol(&self, _symbol: &crate::Symbol) -> bool {
168        false
169    }
170
171    fn embedded_content(&self, _node: &Node, _content: &str) -> Option<crate::EmbeddedBlock> {
172        None
173    }
174
175    fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
176        node.child_by_field_name("content")
177    }
178
179    fn body_has_docstring(&self, _body: &Node, _content: &str) -> bool {
180        false
181    }
182
183    fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
184        // For sections, the title is typically the first line
185        let text = &content[node.byte_range()];
186        let first_line = text.lines().next()?;
187        // Strip section markers (=, ==, etc.)
188        let name = first_line.trim().trim_start_matches('=').trim();
189        if !name.is_empty() { Some(name) } else { None }
190    }
191
192    fn file_path_to_module_name(&self, path: &Path) -> Option<String> {
193        let ext = path.extension()?.to_str()?;
194        if !["adoc", "asciidoc", "asc"].contains(&ext) {
195            return None;
196        }
197        let stem = path.file_stem()?.to_str()?;
198        Some(stem.to_string())
199    }
200
201    fn module_name_to_paths(&self, module: &str) -> Vec<String> {
202        vec![format!("{}.adoc", module), format!("{}.asciidoc", module)]
203    }
204
205    fn lang_key(&self) -> &'static str {
206        "asciidoc"
207    }
208
209    fn is_stdlib_import(&self, _: &str, _: &Path) -> bool {
210        false
211    }
212    fn find_stdlib(&self, _project_root: &Path) -> Option<PathBuf> {
213        None
214    }
215    fn resolve_local_import(&self, _: &str, _: &Path, _: &Path) -> Option<PathBuf> {
216        None
217    }
218    fn resolve_external_import(&self, _: &str, _: &Path) -> Option<ResolvedPackage> {
219        None
220    }
221    fn get_version(&self, _: &Path) -> Option<String> {
222        None
223    }
224    fn find_package_cache(&self, _: &Path) -> Option<PathBuf> {
225        None
226    }
227    fn indexable_extensions(&self) -> &'static [&'static str] {
228        &["adoc", "asciidoc", "asc"]
229    }
230    fn package_sources(&self, _: &Path) -> Vec<crate::PackageSource> {
231        Vec::new()
232    }
233
234    fn should_skip_package_entry(&self, name: &str, is_dir: bool) -> bool {
235        use crate::traits::{has_extension, skip_dotfiles};
236        if skip_dotfiles(name) {
237            return true;
238        }
239        !is_dir && !has_extension(name, self.indexable_extensions())
240    }
241
242    fn discover_packages(&self, _: &crate::PackageSource) -> Vec<(String, PathBuf)> {
243        Vec::new()
244    }
245
246    fn package_module_name(&self, entry_name: &str) -> String {
247        entry_name
248            .strip_suffix(".adoc")
249            .or_else(|| entry_name.strip_suffix(".asciidoc"))
250            .or_else(|| entry_name.strip_suffix(".asc"))
251            .unwrap_or(entry_name)
252            .to_string()
253    }
254
255    fn find_package_entry(&self, path: &Path) -> Option<PathBuf> {
256        if path.is_file() {
257            Some(path.to_path_buf())
258        } else {
259            None
260        }
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267    use crate::validate_unused_kinds_audit;
268
269    #[test]
270    fn unused_node_kinds_audit() {
271        #[rustfmt::skip]
272        let documented_unused: &[&str] = &[
273            // Block types - not symbols
274            "literal_block", "listing_block", "open_block", "quoted_block",
275            "passthrough_block", "delimited_block", "table_block", "ntable_block",
276            "ident_block", "quoted_md_block",
277            // Block markers and bodies
278            "block_comment", "block_comment_start_marker", "block_comment_end_marker",
279            "quoted_block_marker", "quoted_block_md_marker", "passthrough_block_marker",
280            "open_block_marker", "table_block_marker", "ntable_block_marker",
281            "literal_block_marker", "literal_block_body",
282            "listing_block_start_marker", "listing_block_end_marker", "listing_block_body",
283            "delimited_block_start_marker", "delimited_block_end_marker",
284            // Block elements and titles
285            "block_title", "block_title_marker", "block_element",
286            "block_macro_name", "block_macro_attr",
287            // Other content
288            "body", "ident_block_line", "admonition_important",
289        ];
290        validate_unused_kinds_audit(&AsciiDoc, documented_unused)
291            .expect("AsciiDoc unused node kinds audit failed");
292    }
293}