1use crate::external_packages::ResolvedPackage;
4use crate::{Export, Import, Language, Symbol, SymbolKind, Visibility, VisibilityMechanism};
5use std::path::{Path, PathBuf};
6use tree_sitter::Node;
7
8pub 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"] }
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 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 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 let text = &content[node.byte_range()];
186 let first_line = text.lines().next()?;
187 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 "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_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_title", "block_title_marker", "block_element",
286 "block_macro_name", "block_macro_attr",
287 "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}