1use once_cell::sync::Lazy;
2use regex::Regex;
3
4use crate::{
5 common::{
6 arena::{self},
7 error::*,
8 },
9 mouth,
10 parameter::{Parameter, Parameters},
11 pin,
12 token::*,
13 tokens::{TeXString, Tokens},
14};
15
16static CSNAME_MACRO_RE: Lazy<Regex> =
17 Lazy::new(|| Regex::new(r"^\\csname\s+(.*)\\endcsname").unwrap());
18static CS_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(\\[a-zA-Z@_]+(?::[a-zA-Z]*)?)").unwrap());
28static SINGLE_CHAR_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(\\.)").unwrap());
29static ACTIVE_CHAR_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(.)").unwrap());
30
31pub fn parse_prototype(proto: &str, init_flag: bool) -> Result<(Token, Option<Parameters>)> {
33 let cs;
34 let normalized_proto = if let Some(captures) = CSNAME_MACRO_RE.captures(proto) {
35 let csname_content = captures.get(1).map_or("", |m| m.as_str());
36 if !init_flag && (csname_content.contains('{') || csname_content.contains('}')) {
40 panic!(
41 "\\csname...\\endcsname with braces in definition prototype is not supported at compile time: \"{}\". \
42 Use RawTeX!() or runtime DefMacroI() for CS names containing {{}} characters.",
43 proto
44 );
45 }
46 cs = T_CS!(s!("\\{}", csname_content));
47 CSNAME_MACRO_RE.replace(proto, "")
49 } else if let Some(captures) = CS_RE.captures(proto) {
50 let csname = captures.get(1).map_or("", |m| m.as_str()).to_string();
52 cs = T_CS!(csname);
53 CS_RE.replace(proto, "")
55 } else if let Some(captures) = SINGLE_CHAR_RE.captures(proto) {
56 cs = T_CS!(captures.get(1).map_or("", |m| m.as_str()));
58 SINGLE_CHAR_RE.replace(proto, "")
60 } else if let Some(captures) = ACTIVE_CHAR_RE.captures(proto) {
61 cs = mouth::tokenize_internal(TeXString::assembled(
63 captures.get(1).map_or("", |m| m.as_str()).to_string(),
64 ))
65 .unlist()
66 .remove(0);
67 ACTIVE_CHAR_RE.replace(proto, "")
69 } else {
70 let message = s!(
71 "Definition prototype doesn't have proper control sequence: \"{}\"",
72 proto
73 );
74 fatal!(Prototype, Misdefined, message);
75 };
76 let final_proto = normalized_proto.trim();
77 let paramlist = parse_parameters(final_proto, &cs, init_flag)?;
78 Ok((cs, paramlist))
79}
80
81pub fn parse_parameters(
83 outer_prototype: &str,
84 cs: &Token,
85 init_flag: bool,
86) -> Result<Option<Parameters>> {
87 use winnow::{
95 combinator::{delimited, opt, preceded},
96 prelude::*,
97 token::{any, take_while},
98 };
99
100 fn ws0(input: &mut &str) -> ModalResult<()> {
101 take_while(0.., |c: char| c.is_whitespace())
102 .void()
103 .parse_next(input)
104 }
105 fn group<'a>(open: char, close: char) -> impl FnMut(&mut &'a str) -> ModalResult<&'a str> {
107 move |input: &mut &'a str| {
108 let inner = delimited(open, take_while(0.., move |c| c != close), close).parse_next(input)?;
109 ws0(input)?;
110 Ok(inner)
111 }
112 }
113 fn paramspec<'a>(input: &mut &'a str) -> ModalResult<(&'a str, Option<&'a str>)> {
115 let word = take_while(0.., |c: char| c.is_alphanumeric() || c == '_').parse_next(input)?;
116 let extra = opt(preceded(
117 ':',
118 take_while(0.., |c: char| !c.is_whitespace() && c != '{' && c != '['),
119 ))
120 .parse_next(input)?;
121 if word.is_empty() && extra.is_none() {
122 return Err(winnow::error::ErrMode::Backtrack(
123 winnow::error::ContextError::new(),
124 ));
125 }
126 ws0(input)?;
127 Ok((word, extra))
128 }
129
130 let mut parameters = Vec::with_capacity(4);
131 let mut rest: &str = outer_prototype;
132 let input = &mut rest;
133 while !input.is_empty() {
134 let mut probe;
138 let res = {
139 probe = *input;
140 group('{', '}')
141 .parse_next(&mut probe)
142 .inspect(|_| *input = probe)
143 };
144 let mut p: Parameter = match res {
145 Ok(inner_spec) => {
146 let inner: Option<Parameters> = if inner_spec.is_empty() {
148 None
149 } else {
150 parse_parameters(inner_spec, cs, init_flag)?
151 };
152 Parameter {
153 name: pin!("Plain"),
154 spec: arena::pin(format!("{{{inner_spec}}}")),
155 inner: inner.map(|ps| ps.into()).unwrap_or_default(),
156 ..Parameter::default()
157 }
158 },
159 _ => {
160 let res = {
161 probe = *input;
162 group('[', ']')
163 .parse_next(&mut probe)
164 .inspect(|_| *input = probe)
165 };
166 match res {
167 Ok(inner_spec) => {
168 let spec = arena::pin(format!("[{inner_spec}]"));
169 if let Some(default_str) = inner_spec.strip_prefix("Default:") {
170 let extra = if default_str.is_empty() {
171 vec![]
172 } else {
173 vec![mouth::tokenize_internal(TeXString::assembled(
174 default_str.to_string(),
175 ))]
176 };
177 Parameter {
178 name: pin!("Optional"),
179 spec,
180 extra,
181 ..Parameter::default()
182 }
183 } else if !inner_spec.is_empty() {
184 Parameter {
185 name: pin!("Optional"),
186 spec,
187 inner: parse_parameters(inner_spec, cs, init_flag)?
188 .map(|ps| ps.into())
189 .unwrap_or_default(),
190 ..Parameter::default()
191 }
192 } else {
193 Parameter {
194 name: pin!("Optional"),
195 spec,
196 ..Parameter::default()
197 }
198 }
199 },
200 _ => {
201 let res = {
202 probe = *input;
203 paramspec.parse_next(&mut probe).inspect(|_| *input = probe)
204 };
205 match res {
206 Ok((word, extra_opt)) => {
207 let spec_str = match extra_opt {
208 Some(extra) => format!("{word}:{extra}"),
209 None => word.to_string(),
210 };
211 let extra: Vec<Tokens> = match extra_opt {
212 None | Some("") => Vec::new(),
213 Some(extra_str) => extra_str
214 .split('|')
215 .map(|t| {
216 Tokens::new(
217 mouth::tokenize_internal(TeXString::assembled(t.to_string())).unlist(),
218 )
219 })
220 .collect(),
221 };
222 Parameter {
223 name: arena::pin(word),
224 spec: arena::pin(&spec_str),
225 extra,
226 ..Parameter::default()
227 }
228 },
229 _ => {
230 let ch = any.parse_next(input).map_err(
232 |_: winnow::error::ErrMode<winnow::error::ContextError>| {
233 Error::from(s!(
234 "parse_parameters: unreadable prototype tail for {:?}",
235 cs
236 ))
237 },
238 )?;
239 let ch_token = CharToken!(ch, Catcode::OTHER);
240 Parameter {
241 name: pin!("Token"),
242 spec: pin!("Token"),
243 extra: vec![Tokens::new(vec![ch_token])],
244 ..Parameter::default()
245 }
246 },
247 }
248 },
249 }
250 },
251 };
252 if init_flag {
253 p = p.init()?;
254 }
255 parameters.push(p);
256 }
257 if parameters.is_empty() {
258 Ok(None)
259 } else {
260 Ok(Some(Parameters::new(parameters)))
261 }
262}
263
264#[cfg(test)]
265mod golden_tests {
266 fn describe(proto: &str) -> String {
268 match super::parse_parameters(proto, &crate::T_CS!("\\x"), false) {
269 Ok(None) => "None".to_string(),
270 Ok(Some(ps)) => ps
271 .get_parameters()
272 .iter()
273 .map(|p| {
274 format!(
275 "{}:{}/x{}/i{}",
276 crate::common::arena::with(p.name, |s| s.to_string()),
277 crate::common::arena::with(p.spec, |s| s.to_string()),
278 p.extra.len(),
279 p.inner
280 .as_ref()
281 .map(|ps| ps.get_parameters().len())
282 .unwrap_or(0)
283 )
284 })
285 .collect::<Vec<_>>()
286 .join(" | "),
287 Err(e) => format!("ERR:{e}"),
288 }
289 }
290
291 #[test]
295 fn golden_prototype_corpus() {
296 crate::state::set_state(crate::state::State::new(
297 crate::state::StateOptions::default(),
298 ));
299 let golden: &[(&str, &str)] = &[
300 ("{}", "Plain:{}/x0/i0"),
301 ("{}{}", "Plain:{}/x0/i0 | Plain:{}/x0/i0"),
302 ("[]", "Optional:[]/x0/i0"),
303 ("[]{}", "Optional:[]/x0/i0 | Plain:{}/x0/i0"),
304 ("{Number}", "Plain:{Number}/x0/i1"),
305 (
306 "{Float}{Float} {}",
307 "Plain:{Float}/x0/i1 | Plain:{Float}/x0/i1 | Plain:{}/x0/i0",
308 ),
309 (
310 "OptionalMatch:* [][] Semiverbatim",
311 "OptionalMatch:OptionalMatch:*/x1/i0 | Optional:[]/x0/i0 | Optional:[]/x0/i0 | \
312 Semiverbatim:Semiverbatim/x0/i0",
313 ),
314 (
315 "OptionalKeyVals:LST",
316 "OptionalKeyVals:OptionalKeyVals:LST/x1/i0",
317 ),
318 (
319 "RequiredKeyVals:RH {}",
320 "RequiredKeyVals:RequiredKeyVals:RH/x1/i0 | Plain:{}/x0/i0",
321 ),
322 ("Until:\\end", "Until:Until:\\end/x1/i0"),
323 (
324 "XUntil:\\fi {}",
325 "XUntil:XUntil:\\fi/x1/i0 | Plain:{}/x0/i0",
326 ),
327 (
328 "[Default:0]{}",
329 "Optional:[Default:0]/x1/i0 | Plain:{}/x0/i0",
330 ),
331 ("Semiverbatim", "Semiverbatim:Semiverbatim/x0/i0"),
332 (
333 "SkipSpaces {}",
334 "SkipSpaces:SkipSpaces/x0/i0 | Plain:{}/x0/i0",
335 ),
336 ("Digested", "Digested:Digested/x0/i0"),
337 ("DigestedBody", "DigestedBody:DigestedBody/x0/i0"),
338 (
339 "(){}",
340 "Token:Token/x1/i0 | Token:Token/x1/i0 | Plain:{}/x0/i0",
341 ),
342 (
343 "( {Float} , {Float} )",
344 "Token:Token/x1/i0 | Token:Token/x1/i0 | Plain:{Float}/x0/i1 | Token:Token/x1/i0 | \
345 Token:Token/x1/i0 | Plain:{Float}/x0/i1 | Token:Token/x1/i0",
346 ),
347 ("Optional:=Default:9", "Optional:Optional:=Default:9/x1/i0"),
348 ("{Until:;}", "Plain:{Until:;}/x0/i1"),
349 ("+", "Token:Token/x1/i0"),
350 ("Match:- {}", "Match:Match:-/x1/i0 | Plain:{}/x0/i0"),
351 (
354 "OptionalMatch:* {{}} [] {}",
355 "OptionalMatch:OptionalMatch:*/x1/i0 | Plain:{{}/x0/i1 | Token:Token/x1/i0 | Token:Token/x1/i0 | \
356 Optional:[]/x0/i0 | Plain:{}/x0/i0",
357 ),
358 ("{", "Token:Token/x1/i0"),
359 ];
360 for (proto, expected) in golden {
361 let expected = expected.split_whitespace().collect::<Vec<_>>().join(" ");
362 let actual = describe(proto)
363 .split_whitespace()
364 .collect::<Vec<_>>()
365 .join(" ");
366 assert_eq!(actual, expected, "prototype grammar diverged on {proto:?}");
367 }
368 }
369}