badness_parser/parser/
core.rs1use std::collections::HashMap;
8
9use rowan::GreenNode;
10use smol_str::SmolStr;
11
12use crate::declarations::ResolvedDeclarations;
13use crate::parser::grammar;
14use crate::parser::lexer::{LatexFlavor, LexConfig, ParseCtx, lex_with, lex_with_implicit_expl};
15use crate::parser::tree_builder::build_tree;
16use crate::semantic::define::scan_definitions;
17use crate::semantic::signature::builtin;
18use crate::syntax::SyntaxNode;
19
20pub use crate::SyntaxError;
21
22#[derive(Debug, Clone)]
24pub struct Parse {
25 pub green: GreenNode,
26 pub errors: Vec<SyntaxError>,
27}
28
29impl Parse {
30 pub fn syntax(&self) -> SyntaxNode {
32 SyntaxNode::new_root(self.green.clone())
33 }
34}
35
36pub fn parse(input: &str) -> Parse {
49 parse_with_flavor(input, LatexFlavor::Document)
50}
51
52pub fn parse_with_flavor(input: &str, config: impl Into<LexConfig>) -> Parse {
60 parse_with_declarations(input, config, &ResolvedDeclarations::default())
61}
62
63pub fn parse_with_declarations(
79 input: &str,
80 config: impl Into<LexConfig>,
81 declared: &ResolvedDeclarations,
82) -> Parse {
83 parse_with_declarations_resolved(input, config, declared).0
84}
85
86pub fn parse_with_declarations_resolved(
99 input: &str,
100 config: impl Into<LexConfig>,
101 declared: &ResolvedDeclarations,
102) -> (Parse, ParseCtx) {
103 let config = config.into();
104 let mut seed = ParseCtx::default();
105 seed.overlay_declarations(declared);
106
107 let pass1 = parse_with(input, &seed, config);
108 let mut ctx = parse_ctx(&pass1.syntax());
109 ctx.overlay_declarations(declared);
111 if ctx == seed {
112 return (pass1, ctx);
113 }
114 let pass2 = parse_with(input, &ctx, config);
115 (pass2, ctx)
116}
117
118fn parse_with(input: &str, ctx: &ParseCtx, config: LexConfig) -> Parse {
120 let tokens = lex_with(input, ctx, config);
121 let (events, errors) = grammar::parse(&tokens, ctx);
122 let green = build_tree(&tokens, &events);
123 Parse { green, errors }
124}
125
126pub(crate) fn parse_fragment_with_ctx(
132 input: &str,
133 ctx: &ParseCtx,
134 config: LexConfig,
135 implicit_expl: bool,
136) -> Parse {
137 let tokens = lex_with_implicit_expl(input, ctx, config, implicit_expl);
138 let (events, errors) = grammar::parse(&tokens, ctx);
139 let green = build_tree(&tokens, &events);
140 Parse { green, errors }
141}
142
143fn parse_ctx(root: &SyntaxNode) -> ParseCtx {
162 let db = scan_definitions(root);
163 let mut ctx = ParseCtx::default();
164 for name in db.command_names() {
165 match db.command(name) {
166 Some(sig) if sig.verbatim => ctx.insert(SmolStr::new(name), sig.args.to_vec()),
167 Some(_)
170 if builtin()
171 .command(name)
172 .is_some_and(|sig| sig.verbatim || sig.args.iter().any(|arg| arg.verbatim)) =>
173 {
174 ctx.suppress(SmolStr::new(name));
175 }
176 _ => {}
177 }
178 }
179 for name in db.environment_names() {
180 if let Some(sig) = db.environment(name).filter(|sig| sig.verbatim_body) {
181 ctx.insert_environment(SmolStr::new(name), sig.args.to_vec());
182 }
183 }
184 if db.env_begin_aliases().next().is_some() || db.env_end_aliases().next().is_some() {
188 let called = command_call_counts(root);
189 let is_called = |name: &str| called.get(name).is_some_and(|n| *n >= 2);
190 for (name, target) in db.env_begin_aliases() {
191 if is_called(name) {
192 ctx.insert_begin_alias(SmolStr::new(name), SmolStr::new(target));
193 }
194 }
195 for (name, target) in db.env_end_aliases() {
201 if is_called(name) {
202 ctx.insert_end_alias(SmolStr::new(name), SmolStr::new(target));
203 }
204 }
205 }
206 ctx
207}
208
209fn command_call_counts(root: &SyntaxNode) -> HashMap<SmolStr, usize> {
214 let mut counts: HashMap<SmolStr, usize> = HashMap::new();
215 for node in root
216 .descendants()
217 .filter(|n| n.kind() == crate::syntax::SyntaxKind::COMMAND)
218 {
219 if let Some(name) = crate::ast::command_name(&node) {
220 *counts.entry(SmolStr::new(name)).or_default() += 1;
221 }
222 }
223 counts
224}
225
226pub fn reconstruct(input: &str) -> String {
229 parse(input).syntax().to_string()
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235
236 #[test]
237 fn reconstruct_is_identity() {
238 let input = "\\section{Hi}\n\nbody $x^2$ % c\n";
239 assert_eq!(reconstruct(input), input);
240 }
241
242 #[test]
243 fn the_resolved_context_carries_the_second_pass_scan() {
244 let input = "\\newcommand\\shellcmd[1]{\\@makeother\\$#1}\n\\shellcmd{a_$b$}\n";
245 let (parse, ctx) =
246 parse_with_declarations_resolved(input, LatexFlavor::Document, &Default::default());
247
248 assert_ne!(
249 ctx,
250 ParseCtx::default(),
251 "the scan found a verbatim definition, so the context must not be the seed"
252 );
253 let relexed: String = lex_with(input, &ctx, LatexFlavor::Document.into())
254 .iter()
255 .map(|t| t.text.as_str())
256 .collect();
257 assert_eq!(relexed, parse.syntax().to_string());
258 assert!(
259 parse.syntax().to_string().contains("a_$b$"),
260 "the call-site argument should be captured whole"
261 );
262 }
263
264 #[test]
265 fn the_resolved_context_is_the_seed_when_no_scan_contributes() {
266 let input = "\\section{Hi}\n\nplain prose\n";
267 let (_, ctx) =
268 parse_with_declarations_resolved(input, LatexFlavor::Document, &Default::default());
269 assert_eq!(ctx, ParseCtx::default());
270 }
271
272 #[test]
273 fn command_wraps_its_argument_group() {
274 use crate::syntax::SyntaxKind;
275 let parse = parse(r"\a{b}");
276 let command = parse
277 .syntax()
278 .descendants()
279 .find(|n| n.kind() == SyntaxKind::COMMAND)
280 .expect("a COMMAND node");
281 assert!(
282 command.children().any(|n| n.kind() == SyntaxKind::GROUP),
283 "the argument should be a nested GROUP node"
284 );
285 assert!(parse.errors.is_empty());
286 }
287
288 #[test]
289 fn math_environment_body_is_a_math_node() {
290 use crate::syntax::SyntaxKind;
291 let input = "\\begin{equation}\n x_i^2 = y\n\\end{equation}\n";
292 let root = parse(input).syntax();
293 let math = root
294 .descendants()
295 .find(|n| n.kind() == SyntaxKind::MATH)
296 .expect("the equation body is wrapped in a MATH node");
297 assert!(
298 math.descendants().any(|n| n.kind() == SyntaxKind::SCRIPTED),
299 "scripts inside the math environment build SCRIPTED nodes"
300 );
301 assert!(
302 !root
303 .descendants()
304 .any(|n| n.kind() == SyntaxKind::PARAGRAPH),
305 "the body is math, not a prose PARAGRAPH"
306 );
307 assert_eq!(reconstruct(input), input);
308 }
309
310 #[test]
311 fn align_environment_keeps_grid_tokens_inside_math() {
312 use crate::syntax::SyntaxKind;
313 let input = "\\begin{align}\n a &= b \\\\\n c &= d\n\\end{align}\n";
314 let root = parse(input).syntax();
315 let math = root
316 .descendants()
317 .find(|n| n.kind() == SyntaxKind::MATH)
318 .expect("the align body is wrapped in a MATH node");
319 assert!(
320 math.children_with_tokens()
321 .any(|e| e.kind() == SyntaxKind::AMPERSAND),
322 "top-level `&` stays a direct MATH child"
323 );
324 assert!(
325 math.children().any(|n| n.kind() == SyntaxKind::LINE_BREAK),
326 "top-level `\\\\` stays a LINE_BREAK child of MATH"
327 );
328 assert_eq!(reconstruct(input), input);
329 }
330
331 #[test]
332 fn non_math_environment_body_is_unchanged() {
333 use crate::syntax::SyntaxKind;
334 let input = "\\begin{itemize}\n \\item a\n\\end{itemize}\n";
335 let root = parse(input).syntax();
336 assert!(
337 !root.descendants().any(|n| n.kind() == SyntaxKind::MATH),
338 "a text environment never enters math mode"
339 );
340 assert_eq!(reconstruct(input), input);
341 }
342
343 #[test]
344 fn unclosed_math_environment_recovers() {
345 use crate::syntax::SyntaxKind;
346 let input = "\\begin{equation}\n a = b\n";
347 let parse = parse(input);
348 assert!(
349 parse
350 .syntax()
351 .descendants()
352 .any(|n| n.kind() == SyntaxKind::MATH),
353 "the body still parses as math"
354 );
355 assert!(!parse.errors.is_empty(), "an unclosed environment reports");
356 assert_eq!(reconstruct(input), input);
357 }
358}