badness_parser/parser/
core.rs1use rowan::GreenNode;
8use smol_str::SmolStr;
9
10use crate::parser::grammar;
11use crate::parser::lexer::{LatexFlavor, LexConfig, VerbCtx, lex_with};
12use crate::parser::tree_builder::build_tree;
13use crate::semantic::define::scan_definitions;
14use crate::semantic::signature::builtin;
15use crate::syntax::SyntaxNode;
16
17#[derive(Debug, Clone)]
20pub struct Parse {
21 pub green: GreenNode,
22 pub errors: Vec<SyntaxError>,
23}
24
25impl Parse {
26 pub fn syntax(&self) -> SyntaxNode {
29 SyntaxNode::new_root(self.green.clone())
30 }
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct SyntaxError {
36 pub message: String,
37 pub start: usize,
38 pub end: usize,
39}
40
41pub fn parse(input: &str) -> Parse {
54 parse_with_flavor(input, LatexFlavor::Document)
55}
56
57pub fn parse_with_flavor(input: &str, config: impl Into<LexConfig>) -> Parse {
65 let config = config.into();
66 let pass1 = parse_with(input, &VerbCtx::default(), config);
67 let ctx = verbatim_ctx(&pass1.syntax());
68 if ctx.is_empty() {
69 return pass1;
70 }
71 parse_with(input, &ctx, config)
72}
73
74fn parse_with(input: &str, ctx: &VerbCtx, config: LexConfig) -> Parse {
76 let tokens = lex_with(input, ctx, config);
77 let (events, errors) = grammar::parse(&tokens, ctx);
78 let green = build_tree(&tokens, &events);
79 Parse { green, errors }
80}
81
82fn verbatim_ctx(root: &SyntaxNode) -> VerbCtx {
93 let db = scan_definitions(root);
94 let mut ctx = VerbCtx::default();
95 for name in db.command_names() {
96 match db.command(name) {
97 Some(sig) if sig.verbatim => ctx.insert(SmolStr::new(name), sig.args.to_vec()),
98 Some(_) if builtin().command(name).is_some_and(|sig| sig.verbatim) => {
101 ctx.suppress(SmolStr::new(name));
102 }
103 _ => {}
104 }
105 }
106 for name in db.environment_names() {
107 if let Some(sig) = db.environment(name).filter(|sig| sig.verbatim_body) {
108 ctx.insert_environment(SmolStr::new(name), sig.args.to_vec());
109 }
110 }
111 ctx
112}
113
114pub fn reconstruct(input: &str) -> String {
117 parse(input).syntax().to_string()
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 #[test]
125 fn reconstruct_is_identity() {
126 let input = "\\section{Hi}\n\nbody $x^2$ % c\n";
127 assert_eq!(reconstruct(input), input);
128 }
129
130 #[test]
131 fn command_wraps_its_argument_group() {
132 use crate::syntax::SyntaxKind;
133 let parse = parse(r"\a{b}");
134 let command = parse
135 .syntax()
136 .descendants()
137 .find(|n| n.kind() == SyntaxKind::COMMAND)
138 .expect("a COMMAND node");
139 assert!(
140 command.children().any(|n| n.kind() == SyntaxKind::GROUP),
141 "the argument should be a nested GROUP node"
142 );
143 assert!(parse.errors.is_empty());
144 }
145
146 #[test]
151 fn math_environment_body_is_a_math_node() {
152 use crate::syntax::SyntaxKind;
153 let input = "\\begin{equation}\n x_i^2 = y\n\\end{equation}\n";
154 let root = parse(input).syntax();
155 let math = root
156 .descendants()
157 .find(|n| n.kind() == SyntaxKind::MATH)
158 .expect("the equation body is wrapped in a MATH node");
159 assert!(
160 math.descendants().any(|n| n.kind() == SyntaxKind::SCRIPTED),
161 "scripts inside the math environment build SCRIPTED nodes"
162 );
163 assert!(
164 !root
165 .descendants()
166 .any(|n| n.kind() == SyntaxKind::PARAGRAPH),
167 "the body is math, not a prose PARAGRAPH"
168 );
169 assert_eq!(reconstruct(input), input);
170 }
171
172 #[test]
176 fn align_environment_keeps_grid_tokens_inside_math() {
177 use crate::syntax::SyntaxKind;
178 let input = "\\begin{align}\n a &= b \\\\\n c &= d\n\\end{align}\n";
179 let root = parse(input).syntax();
180 let math = root
181 .descendants()
182 .find(|n| n.kind() == SyntaxKind::MATH)
183 .expect("the align body is wrapped in a MATH node");
184 assert!(
185 math.children_with_tokens()
186 .any(|e| e.kind() == SyntaxKind::AMPERSAND),
187 "top-level `&` stays a direct MATH child"
188 );
189 assert!(
190 math.children().any(|n| n.kind() == SyntaxKind::LINE_BREAK),
191 "top-level `\\\\` stays a LINE_BREAK child of MATH"
192 );
193 assert_eq!(reconstruct(input), input);
194 }
195
196 #[test]
199 fn non_math_environment_body_is_unchanged() {
200 use crate::syntax::SyntaxKind;
201 let input = "\\begin{itemize}\n \\item a\n\\end{itemize}\n";
202 let root = parse(input).syntax();
203 assert!(
204 !root.descendants().any(|n| n.kind() == SyntaxKind::MATH),
205 "a text environment never enters math mode"
206 );
207 assert_eq!(reconstruct(input), input);
208 }
209
210 #[test]
213 fn unclosed_math_environment_recovers() {
214 use crate::syntax::SyntaxKind;
215 let input = "\\begin{equation}\n a = b\n";
216 let parse = parse(input);
217 assert!(
218 parse
219 .syntax()
220 .descendants()
221 .any(|n| n.kind() == SyntaxKind::MATH),
222 "the body still parses as math"
223 );
224 assert!(!parse.errors.is_empty(), "an unclosed environment reports");
225 assert_eq!(reconstruct(input), input);
226 }
227}