1use rowan::TextRange;
25
26use crate::ast::{
27 AstNode, Begin, Environment, command_name, first_group_range, group_command_name, nth_group,
28};
29use crate::syntax::{SyntaxKind, SyntaxNode};
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum DocKind {
34 Macro,
36 Environment,
38 DescribeMacro,
40 DescribeEnv,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct DocAssociation {
48 pub name: String,
51 pub kind: DocKind,
52 pub range: TextRange,
54 pub name_range: TextRange,
56 pub code: Vec<TextRange>,
60}
61
62pub fn doc_associations(root: &SyntaxNode) -> Vec<DocAssociation> {
64 let mut out = Vec::new();
65 collect(root, &mut out);
66 out
67}
68
69fn collect(node: &SyntaxNode, out: &mut Vec<DocAssociation>) {
73 for child in node.children() {
74 match child.kind() {
75 SyntaxKind::COMMAND => collect_command(&child, out),
76 SyntaxKind::ENVIRONMENT => collect_environment(&child, out),
77 _ => collect(&child, out),
78 }
79 }
80}
81
82fn collect_environment(env: &SyntaxNode, out: &mut Vec<DocAssociation>) {
86 let begin = Environment::cast(env.clone()).and_then(|e| e.begin());
88 let kind = begin
89 .as_ref()
90 .and_then(Begin::name)
91 .and_then(|name| match name.as_str() {
92 "macro" => Some(DocKind::Macro),
93 "environment" => Some(DocKind::Environment),
94 _ => None,
95 });
96
97 if let (Some(kind), Some(begin)) = (kind, begin.as_ref())
98 && let Some(group) = nth_group(begin.syntax(), 0)
99 && let Some(name) = documented_name(&group, kind)
100 {
101 let mut code = Vec::new();
102 collect_code(env, &mut code);
103 out.push(DocAssociation {
104 name,
105 kind,
106 range: env.text_range(),
107 name_range: group.text_range(),
108 code,
109 });
110 }
111
112 collect(env, out);
115}
116
117fn collect_command(command: &SyntaxNode, out: &mut Vec<DocAssociation>) {
119 let Some(cmd) = command_name(command) else {
120 return;
121 };
122 let kind = match cmd.as_str() {
123 "DescribeMacro" => DocKind::DescribeMacro,
124 "DescribeEnv" => DocKind::DescribeEnv,
125 _ => return,
126 };
127
128 if let Some(group) = nth_group(command, 0) {
133 if let Some(name) = documented_name(&group, kind) {
134 out.push(DocAssociation {
135 name,
136 kind,
137 range: first_group_range(command),
138 name_range: group.text_range(),
139 code: Vec::new(),
140 });
141 }
142 } else if kind == DocKind::DescribeMacro
143 && let Some(sib) = command.next_sibling()
144 && sib.kind() == SyntaxKind::COMMAND
145 && let Some(name) = command_name(&sib)
146 {
147 out.push(DocAssociation {
148 name: format!("\\{name}"),
149 kind,
150 range: TextRange::new(command.text_range().start(), sib.text_range().end()),
151 name_range: sib.text_range(),
152 code: Vec::new(),
153 });
154 }
155}
156
157fn documented_name(group: &SyntaxNode, kind: DocKind) -> Option<String> {
162 match kind {
163 DocKind::Macro | DocKind::DescribeMacro => {
164 group_command_name(group).map(|name| format!("\\{name}"))
165 }
166 DocKind::Environment | DocKind::DescribeEnv => {
167 let text = group_inner_text(group)?;
171 let text = text.trim();
172 (!text.is_empty()).then(|| text.to_owned())
173 }
174 }
175}
176
177fn group_inner_text(group: &SyntaxNode) -> Option<String> {
182 let mut text = String::new();
183 for element in group.children_with_tokens() {
184 match element {
185 rowan::NodeOrToken::Token(token) => match token.kind() {
186 SyntaxKind::L_BRACE | SyntaxKind::R_BRACE => {}
187 SyntaxKind::HASH => return None,
188 _ => text.push_str(token.text()),
189 },
190 rowan::NodeOrToken::Node(_) => return None,
191 }
192 }
193 Some(text)
194}
195
196fn collect_code(node: &SyntaxNode, out: &mut Vec<TextRange>) {
199 for child in node.children() {
200 if child.kind() == SyntaxKind::ENVIRONMENT {
201 let name = Environment::cast(child.clone())
202 .and_then(|e| e.begin())
203 .and_then(|b| b.name());
204 match name.as_deref() {
205 Some("macrocode" | "macrocode*") => out.push(child.text_range()),
206 Some("macro" | "environment") => {}
208 _ => collect_code(&child, out),
209 }
210 } else {
211 collect_code(&child, out);
212 }
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219 use crate::parser::{LatexFlavor, LexConfig, parse_with_flavor};
220
221 fn assoc_of(src: &str) -> Vec<DocAssociation> {
222 let config = LexConfig {
223 flavor: LatexFlavor::Document,
224 dtx: true,
225 };
226 let parsed = parse_with_flavor(src, config);
227 assert_eq!(parsed.syntax().to_string(), src, "losslessness violated");
228 doc_associations(&parsed.syntax())
229 }
230
231 #[test]
232 fn macro_env_documents_its_name() {
233 let items = assoc_of("% \\begin{macro}{\\foo}\n% docs.\n% \\end{macro}\n");
234 assert_eq!(items.len(), 1);
235 assert_eq!(items[0].name, "\\foo");
236 assert_eq!(items[0].kind, DocKind::Macro);
237 assert!(items[0].code.is_empty());
238 }
239
240 #[test]
241 fn nested_macrocode_is_the_code() {
242 let src = "% \\begin{macro}{\\foo}\n% docs.\n% \\begin{macrocode}\n\\def\\foo{x}\n% \\end{macrocode}\n% \\end{macro}\n";
243 let items = assoc_of(src);
244 assert_eq!(items.len(), 1);
245 assert_eq!(items[0].name, "\\foo");
246 assert_eq!(items[0].code.len(), 1);
247 let code = &src[items[0].code[0]];
249 assert!(code.starts_with("\\begin{macrocode}"));
250 assert!(code.contains("\\def\\foo{x}"));
251 }
252
253 #[test]
254 fn environment_env_documents_a_plain_name() {
255 let items = assoc_of("% \\begin{environment}{myenv}\n% docs.\n% \\end{environment}\n");
256 assert_eq!(items.len(), 1);
257 assert_eq!(items[0].name, "myenv");
258 assert_eq!(items[0].kind, DocKind::Environment);
259 }
260
261 #[test]
262 fn describe_macro_braced() {
263 let items = assoc_of("% \\DescribeMacro{\\foo} does foo.\n");
264 assert_eq!(items.len(), 1);
265 assert_eq!(items[0].name, "\\foo");
266 assert_eq!(items[0].kind, DocKind::DescribeMacro);
267 assert!(items[0].code.is_empty());
268 }
269
270 #[test]
271 fn describe_macro_braceless() {
272 let items = assoc_of("% \\DescribeMacro\\foo does foo.\n");
274 assert_eq!(items.len(), 1);
275 assert_eq!(items[0].name, "\\foo");
276 assert_eq!(items[0].kind, DocKind::DescribeMacro);
277 }
278
279 #[test]
280 fn describe_env() {
281 let items = assoc_of("% \\DescribeEnv{myenv} is an env.\n");
282 assert_eq!(items.len(), 1);
283 assert_eq!(items[0].name, "myenv");
284 assert_eq!(items[0].kind, DocKind::DescribeEnv);
285 }
286
287 #[test]
288 fn nested_macro_envs_both_surface_with_own_code() {
289 let src = "% \\begin{macro}{\\outer}\n% \\begin{macrocode}\n\\def\\outer{o}\n% \\end{macrocode}\n% \\begin{macro}{\\inner}\n% \\begin{macrocode}\n\\def\\inner{i}\n% \\end{macrocode}\n% \\end{macro}\n% \\end{macro}\n";
290 let items = assoc_of(src);
291 let names: Vec<_> = items.iter().map(|i| i.name.as_str()).collect();
292 assert_eq!(names, vec!["\\outer", "\\inner"]);
293 let outer = &items[0];
296 assert_eq!(outer.code.len(), 1);
297 assert!(src[outer.code[0]].contains("\\def\\outer{o}"));
298 let inner = &items[1];
299 assert_eq!(inner.code.len(), 1);
300 assert!(src[inner.code[0]].contains("\\def\\inner{i}"));
301 }
302
303 #[test]
304 fn empty_or_nested_macro_name_is_skipped() {
305 assert!(assoc_of("% \\begin{macro}{}\n% \\end{macro}\n").is_empty());
306 assert!(assoc_of("% \\DescribeEnv{\\foo}\n").is_empty());
309 }
310
311 #[test]
312 fn non_ltxdoc_constructs_are_ignored() {
313 let items =
314 assoc_of("% \\section{Intro}\n% \\begin{itemize}\n% \\item x\n% \\end{itemize}\n");
315 assert!(items.is_empty());
316 }
317}