Skip to main content

pulldown_latex/parser/
lex.rs

1use crate::event::{DelimiterType, Dimension, DimensionUnit, Glue, GroupingKind, Line};
2
3use super::{
4    tables::{dvipsnames_color, primitive_color, token_to_delim},
5    Argument, CharToken, ErrorKind, InnerResult, Token,
6};
7
8/// Parse the right-hand side of a definition (TeXBook p. 271).
9///
10/// In this case, a definition is any of `def`, `edef`, `gdef`, or `xdef`.
11///
12/// Returns the control sequence, the parameter text, and the replacement text.
13pub fn definition<'a>(input: &mut &'a str) -> InnerResult<(&'a str, &'a str, &'a str)> {
14    let control_sequence = control_sequence(input)?;
15    let (parameter_text, rest) = input.split_once('{').ok_or(ErrorKind::MissingExpansion)?;
16
17    if let Some(idx) = parameter_text.find(['%', '}']) {
18        return Err(if parameter_text.as_bytes()[idx] == b'%' {
19            ErrorKind::CommentInParamText
20        } else {
21            ErrorKind::BracesInParamText
22        });
23    }
24
25    *input = rest;
26    let replacement_text = group_content(input, GroupingKind::Normal)?;
27
28    Ok((control_sequence, parameter_text, replacement_text))
29}
30
31/// Parse an argument to a control sequence, and return it.
32pub fn argument<'a>(input: &mut &'a str) -> InnerResult<Argument<'a>> {
33    if let Some(rest) = input.trim_start().strip_prefix('{') {
34        *input = rest;
35        let content = group_content(input, GroupingKind::Normal)?;
36        Ok(Argument::Group(content))
37    } else {
38        Ok(Argument::Token(token(input)?))
39    }
40}
41
42pub fn optional_argument<'a>(input: &mut &'a str) -> Option<&'a str> {
43    if let Some(rest) = input.trim_start().strip_prefix('[') {
44        *input = rest;
45        let content = group_content(input, GroupingKind::OptionalArgument).ok()?;
46        Some(content)
47    } else {
48        None
49    }
50}
51
52pub fn brace_argument<'a>(input: &mut &'a str) -> InnerResult<&'a str> {
53    if let Some(rest) = input.trim_start().strip_prefix('{') {
54        *input = rest;
55        group_content(input, GroupingKind::Normal)
56    } else {
57        Err(ErrorKind::GroupArgument)
58    }
59}
60
61/// Parses the inside of a group, when the first opening tag is already parsed.
62///
63/// The output is the content within the group without the surrounding `start` and `end`.
64/// This content is guaranteed to be balanced.
65pub fn group_content<'a>(input: &mut &'a str, grouping_kind: GroupingKind) -> InnerResult<&'a str> {
66    let start = grouping_kind.opening_str();
67    let end = grouping_kind.closing_str();
68    let mut escaped = false;
69    let mut index = 0;
70    let mut depth = 0u32;
71    let bytes = input.as_bytes();
72    while escaped || depth > 0 || !bytes[index..].starts_with(end.as_bytes()) {
73        if index + end.len() > input.len() {
74            *input = &input[input.len()..];
75            return Err(ErrorKind::UnbalancedGroup(Some(grouping_kind)));
76        }
77        if !escaped && bytes[index..].starts_with(start.as_bytes()) {
78            depth += 1;
79            index += start.len();
80            continue;
81        }
82        if !escaped && bytes[index..].starts_with(end.as_bytes()) {
83            if depth.checked_sub(1).is_none() {
84                break;
85            }
86            depth -= 1;
87            index += end.len();
88            continue;
89        }
90        match bytes[index] {
91            b'\\' => escaped = !escaped,
92            b'%' if !escaped => {
93                let Some(rest_pos) = bytes[index..].iter().position(|&c| c == b'\n') else {
94                    return Err(ErrorKind::UnbalancedGroup(Some(grouping_kind)));
95                };
96                index += rest_pos;
97            }
98            _ => escaped = false,
99        }
100        index += 1;
101    }
102    let (argument, rest) = input.split_at(index);
103    *input = &rest[end.len()..];
104    Ok(argument)
105}
106
107/// Finds the shortest prefix of `input` that contains balanced groups and ends with the given `suffix`.
108pub fn content_with_suffix<'a>(input: &mut &'a str, suffix: &str) -> InnerResult<&'a str> {
109    let mut escaped = false;
110    let mut index = 0;
111    let bytes = input.as_bytes();
112    while escaped || {
113        if index + suffix.len() > input.len() {
114            *input = &input[input.len()..];
115            return Err(ErrorKind::MacroSuffixNotFound);
116        }
117        !bytes[index..].starts_with(suffix.as_bytes())
118    } {
119        match bytes[index] {
120            b'\\' => escaped = !escaped,
121            b'%' if !escaped => {
122                let rest_pos = bytes[index..]
123                    .iter()
124                    .position(|&c| c == b'\n')
125                    .unwrap_or(bytes.len());
126                index += rest_pos;
127            }
128            b'{' if !escaped => {
129                let content = group_content(&mut &input[index + 1..], GroupingKind::Normal)?;
130                index += content.len() + 1;
131            }
132            _ => escaped = false,
133        }
134        index += 1;
135    }
136    let (argument, rest) = input.split_at(index);
137    *input = &rest[suffix.len()..];
138    Ok(argument)
139}
140
141/// Converts a control sequence or character into its corresponding delimiter unicode
142/// character, and whether or not the delimiter is an opening.
143///
144/// Current delimiters supported are listed in TeXBook p. 146, and on https://temml.org/docs/en/supported ("delimiter" section).
145pub fn delimiter(input: &mut &str) -> InnerResult<(char, DelimiterType)> {
146    let maybe_delim = token(input)?;
147    token_to_delim(maybe_delim).ok_or(ErrorKind::Delimiter)
148}
149
150/// Parse the right-hand side of a `futurelet` assignment (TeXBook p. 273).
151///
152/// Returns the control sequence, the token it should be assigned to, and the rest of the input
153/// with both tokens not consumed.
154pub fn futurelet_assignment<'a>(input: &mut &'a str) -> InnerResult<(&'a str, Token<'a>, &'a str)> {
155    let control_sequence = control_sequence(input)?;
156
157    let input_with_tokens = *input;
158
159    let _ = token(input)?;
160    let token = token(input)?;
161    Ok((control_sequence, token, input_with_tokens))
162}
163
164/// Parse the right-hand side of a `let` assignment (TeXBook p. 273).
165///
166/// Returns the control sequence and the value it is assigned to.
167pub fn let_assignment<'a>(input: &mut &'a str) -> InnerResult<(&'a str, Token<'a>)> {
168    let control_sequence = control_sequence(input)?;
169    if let Some(s) = input.trim_start().strip_prefix('=') {
170        *input = s;
171    }
172    let token = token(input)?;
173    Ok((control_sequence, token))
174}
175
176/// Parse a control_sequence, including the leading `\`.
177pub fn control_sequence<'a>(input: &mut &'a str) -> InnerResult<&'a str> {
178    if let Some(rest) = input.strip_prefix('\\') {
179        *input = rest;
180        rhs_control_sequence(input)
181    } else {
182        input
183            .chars()
184            .next()
185            .map_or(Err(ErrorKind::EmptyControlSequence), |_| {
186                Err(ErrorKind::ControlSequence)
187            })
188    }
189}
190
191pub fn limit_modifiers(input: &mut &str) -> Option<bool> {
192    let mut output = None;
193    while let Some((rest, limits)) = input
194        .trim_start()
195        .strip_prefix(r"\limits")
196        .map(|rest| (rest, true))
197        .or_else(|| {
198            input
199                .trim_start()
200                .strip_prefix(r"\nolimits")
201                .map(|rest| (rest, false))
202        })
203    {
204        *input = rest;
205        output = Some(limits);
206    }
207    output
208}
209
210/// Parse the right side of a control sequence (`\` already being parsed).
211///
212/// A control sequence can be of the form `\controlsequence`, or `\#` (control symbol).
213pub fn rhs_control_sequence<'a>(input: &mut &'a str) -> InnerResult<&'a str> {
214    let first_char_byte_count = input
215        .chars()
216        .next()
217        .ok_or(ErrorKind::EmptyControlSequence)?
218        .len_utf8();
219
220    let len = input
221        .chars()
222        .take_while(|c| c.is_ascii_alphabetic())
223        .count()
224        .max(first_char_byte_count);
225
226    let (control_sequence, rest) = input.split_at(len);
227    *input = rest.trim_start();
228    Ok(control_sequence)
229}
230
231/// Parse a glue (TeXBook p. 267).
232pub fn glue(input: &mut &str) -> InnerResult<Glue> {
233    let mut dimen = (dimension(input)?, None, None);
234    if let Some(s) = input.trim_start().strip_prefix("plus") {
235        *input = s;
236        dimen.1 = Some(dimension(input)?);
237    }
238    if let Some(s) = input.trim_start().strip_prefix("minus") {
239        *input = s;
240        dimen.2 = Some(dimension(input)?);
241    }
242    Ok(dimen)
243}
244
245/// Parse a glue, accepting a single `{...}` group around it as KaTeX/MathJax do.
246///
247/// Plain TeX requires the bare form (e.g. `\hskip 1em`), but KaTeX and MathJax also
248/// tolerate `\hskip{1em}`. This wrapper transparently unwraps a single brace group.
249pub fn glue_or_braced(input: &mut &str) -> InnerResult<Glue> {
250    if input.trim_start().starts_with('{') {
251        let mut inner = brace_argument(input)?;
252        let result = glue(&mut inner)?;
253        // Allow any trailing whitespace inside the group.
254        if !inner.trim_start().is_empty() {
255            return Err(ErrorKind::DimensionArgument);
256        }
257        Ok(result)
258    } else {
259        glue(input)
260    }
261}
262
263/// Parse a dimension (TeXBook p. 266).
264pub fn dimension(input: &mut &str) -> InnerResult<Dimension> {
265    let number = floating_point(input)?;
266    let unit = dimension_unit(input)?;
267    Ok(Dimension::new(number, unit))
268}
269
270/// Parse a dimension, accepting a single `{...}` group around it as KaTeX/MathJax do.
271///
272/// Plain TeX requires the bare form (e.g. `\kern 1em`), but KaTeX and MathJax also
273/// tolerate `\kern{1em}`. This wrapper transparently unwraps a single brace group.
274pub fn dimension_or_braced(input: &mut &str) -> InnerResult<Dimension> {
275    if input.trim_start().starts_with('{') {
276        let mut inner = brace_argument(input)?;
277        let result = dimension(&mut inner)?;
278        if !inner.trim_start().is_empty() {
279            return Err(ErrorKind::DimensionArgument);
280        }
281        Ok(result)
282    } else {
283        dimension(input)
284    }
285}
286
287/// Parse a dimension unit (TeXBook p. 266).
288pub fn dimension_unit(input: &mut &str) -> InnerResult<DimensionUnit> {
289    *input = input.trim_start();
290    let unit = input.get(0..2).ok_or(ErrorKind::DimensionUnit)?;
291    let unit = match unit {
292        "em" => DimensionUnit::Em,
293        "ex" => DimensionUnit::Ex,
294        "pt" => DimensionUnit::Pt,
295        "pc" => DimensionUnit::Pc,
296        "in" => DimensionUnit::In,
297        "bp" => DimensionUnit::Bp,
298        "cm" => DimensionUnit::Cm,
299        "mm" => DimensionUnit::Mm,
300        "dd" => DimensionUnit::Dd,
301        "cc" => DimensionUnit::Cc,
302        "sp" => DimensionUnit::Sp,
303        "mu" => DimensionUnit::Mu,
304        _ => return Err(ErrorKind::DimensionUnit),
305    };
306
307    *input = &input[2..];
308    one_optional_space(input);
309
310    Ok(unit)
311}
312
313/// Parse an integer that may be positive or negative and may be represented as octal, decimal,
314/// hexadecimal, or a character code (TeXBook p. 265).
315// For future use maybe.
316#[allow(dead_code)]
317pub fn integer(input: &mut &str) -> InnerResult<isize> {
318    let signum = signs(input)?;
319
320    let unsigned_int = unsigned_integer(input)?;
321
322    Ok(unsigned_int as isize * signum)
323}
324
325pub fn unsigned_integer(input: &mut &str) -> InnerResult<usize> {
326    // The following character must be ascii.
327    let next_char = input.chars().next().ok_or(ErrorKind::Number)?;
328    if next_char.is_ascii_digit() {
329        return Ok(decimal(input));
330    }
331    *input = &input[1..];
332    match next_char {
333        '`' => {
334            let mut next_byte = *input.as_bytes().first().ok_or(ErrorKind::Number)?;
335            if next_byte == b'\\' {
336                *input = &input[1..];
337                next_byte = *input.as_bytes().first().ok_or(ErrorKind::Number)?;
338            }
339            if next_byte.is_ascii() {
340                *input = &input[1..];
341                Ok(next_byte as usize)
342            } else {
343                Err(ErrorKind::CharacterNumber)
344            }
345        }
346        '\'' => Ok(octal(input)),
347        '"' => Ok(hexadecimal(input)),
348        _ => Err(ErrorKind::Number),
349    }
350}
351
352/// Parse the signs in front of a number, returning the signum.
353pub fn signs(input: &mut &str) -> InnerResult<isize> {
354    let mut minus_count = 0;
355    *input = input
356        .trim_start_matches(|c: char| {
357            if c == '-' {
358                minus_count += 1;
359                true
360            } else {
361                c == '+' || c.is_whitespace()
362            }
363        })
364        .trim_start();
365    Ok(if minus_count % 2 == 0 { 1 } else { -1 })
366}
367
368/// Parse a base 16 unsigned number.
369pub fn hexadecimal(input: &mut &str) -> usize {
370    let mut number = 0;
371    *input = input.trim_start_matches(|c: char| {
372        if c.is_ascii_alphanumeric() && c < 'G' {
373            number =
374                number * 16 + c.to_digit(16).expect("the character is a valid hex digit") as usize;
375            true
376        } else {
377            false
378        }
379    });
380    one_optional_space(input);
381
382    number
383}
384
385/// Parse a floating point number (named `factor` in TeXBook p. 266).
386pub fn floating_point(input: &mut &str) -> InnerResult<f32> {
387    let signum = signs(input)?;
388
389    let mut number = 0.;
390    *input = input.trim_start_matches(|c: char| {
391        if c.is_ascii_digit() {
392            number = number * 10. + (c as u8 - b'0') as f32;
393            true
394        } else {
395            false
396        }
397    });
398
399    if let Some(stripped_decimal_point) = input.strip_prefix(|c| c == '.' || c == ',') {
400        let mut decimal = 0.;
401        let mut decimal_divisor = 1.;
402        *input = stripped_decimal_point.trim_start_matches(|c: char| {
403            if c.is_ascii_digit() {
404                decimal = decimal * 10. + (c as u8 - b'0') as f32;
405                decimal_divisor *= 10.;
406                true
407            } else {
408                false
409            }
410        });
411        number += decimal / decimal_divisor;
412    };
413
414    Ok(signum as f32 * number)
415}
416
417/// Parse a base 10 unsigned number.
418pub fn decimal(input: &mut &str) -> usize {
419    let mut number = 0;
420    *input = input.trim_start_matches(|c: char| {
421        if c.is_ascii_digit() {
422            number = number * 10 + (c as u8 - b'0') as usize;
423            true
424        } else {
425            false
426        }
427    });
428    one_optional_space(input);
429
430    number
431}
432
433/// Parse a base 8 unsigned number.
434pub fn octal(input: &mut &str) -> usize {
435    let mut number = 0;
436    *input = input.trim_start_matches(|c: char| {
437        if c.is_ascii_digit() {
438            number = number * 8 + (c as u8 - b'0') as usize;
439            true
440        } else {
441            false
442        }
443    });
444    one_optional_space(input);
445
446    number
447}
448
449/// Parse an optional space.
450pub fn one_optional_space(input: &mut &str) -> bool {
451    let mut chars = input.chars();
452    if chars.next().is_some_and(|c| c.is_whitespace()) {
453        *input = &input[1..];
454        true
455    } else {
456        false
457    }
458}
459
460/// Return the next token in the input.
461///
462/// A token will never be whitespace, and will never be inside of a comment.
463pub fn token<'a>(input: &mut &'a str) -> InnerResult<Token<'a>> {
464    *input = input.trim_start();
465    match input.chars().next() {
466        Some('\\') => {
467            *input = &input[1..];
468            Ok(Token::ControlSequence(rhs_control_sequence(input)?))
469        }
470        Some('%') => {
471            let (_, rest) = input
472                .split_once('\n')
473                .unwrap_or(("", &input[input.len()..]));
474            *input = rest;
475            token(input)
476        }
477        Some(c) => {
478            let context = *input;
479            *input = input.split_at(c.len_utf8()).1;
480            Ok(Token::Character(CharToken::from_str(context)))
481        }
482        None => Err(ErrorKind::Token),
483    }
484}
485
486pub fn color(color: &str) -> Option<(u8, u8, u8)> {
487    match color.strip_prefix('#') {
488        Some(color) if color.len() == 6 => {
489            let r = u8::from_str_radix(&color[..2], 16).ok()?;
490            let g = u8::from_str_radix(&color[2..4], 16).ok()?;
491            let b = u8::from_str_radix(&color[4..], 16).ok()?;
492            Some((r, g, b))
493        }
494        // dvipsnames are case-sensitive (e.g. `Blue` vs CSS `blue`), so try
495        // the exact-case lookup before falling back to the case-insensitive
496        // CSS named-color table.
497        None => dvipsnames_color(color).or_else(|| primitive_color(color)),
498        _ => None,
499    }
500}
501
502pub fn horizontal_lines(content: &mut &str) -> Box<[Line]> {
503    let mut horizontal_lines = Vec::new();
504    while let Some((rest, line)) = content
505        .trim_start()
506        .strip_prefix("\\hline")
507        .map(|rest| (rest, Line::Solid))
508        .or_else(|| {
509            content
510                .trim_start()
511                .strip_prefix("\\hdashline")
512                .map(|rest| (rest, Line::Dashed))
513        })
514    {
515        horizontal_lines.push(line);
516        *content = rest;
517    }
518
519    horizontal_lines.into()
520}
521
522#[cfg(test)]
523mod tests {
524    use crate::{
525        event::{Dimension, DimensionUnit, GroupingKind},
526        parser::{lex, Token},
527    };
528
529    #[test]
530    fn signs() {
531        let mut input = "  +    +-   \\test";
532        assert_eq!(lex::signs(&mut input).unwrap(), -1);
533        assert_eq!(input, "\\test");
534    }
535
536    #[test]
537    fn no_signs() {
538        let mut input = "\\mycommand";
539        assert_eq!(lex::signs(&mut input).unwrap(), 1);
540        assert_eq!(input, "\\mycommand");
541    }
542
543    // A complex exanple from problem 20.7 in TeXBook (p. 205):
544    // \def\cs AB#1#2C$#3\$ {#3{ab#1}#1 c##\x #2}
545    #[test]
546    fn definition_texbook() {
547        let mut input = "\\cs AB#1#2C$#3\\$ {#3{ab#1}#1 c##\\x #2}";
548
549        let (cs, param, repl) = lex::definition(&mut input).unwrap();
550        assert_eq!(cs, "cs");
551        assert_eq!(param, "AB#1#2C$#3\\$ ");
552        assert_eq!(repl, "#3{ab#1}#1 c##\\x #2");
553        assert_eq!(input, "");
554    }
555
556    #[test]
557    fn complex_definition() {
558        let mut input = r"\foo #1\test#2#{##\####2#2 \{{\}} \{\{\{} 5 + 5 = 10";
559        let (cs, param, repl) = lex::definition(&mut input).unwrap();
560
561        assert_eq!(cs, "foo");
562        assert_eq!(param, r"#1\test#2#");
563        assert_eq!(repl, r"##\####2#2 \{{\}} \{\{\{");
564        assert_eq!(input, " 5 + 5 = 10");
565    }
566
567    #[test]
568    fn let_assignment() {
569        let mut input = r"\foo = \bar";
570        let (cs, token) = lex::let_assignment(&mut input).unwrap();
571
572        assert_eq!(cs, "foo");
573        assert_eq!(token, Token::ControlSequence("bar"));
574        assert_eq!(input, "");
575    }
576
577    #[test]
578    fn futurelet_assignment() {
579        let mut input = r"\foo\bar\baz blah";
580        let (cs, token, rest) = lex::futurelet_assignment(&mut input).unwrap();
581
582        assert_eq!(cs, "foo");
583        assert_eq!(token, Token::ControlSequence("baz"));
584        assert_eq!(rest, r"\bar\baz blah");
585    }
586
587    #[test]
588    fn dimension() {
589        let mut input = "1.2pt";
590        let dim = lex::dimension(&mut input).unwrap();
591
592        assert_eq!(dim, Dimension::new(1.2, DimensionUnit::Pt));
593        assert_eq!(input, "");
594    }
595
596    #[test]
597    fn complex_glue() {
598        let mut input = "1.2 pt plus 3.4pt minus 5.6pt nope";
599        let glue = lex::glue(&mut input).unwrap();
600
601        assert_eq!(
602            glue,
603            (
604                Dimension::new(1.2, DimensionUnit::Pt),
605                Some(Dimension::new(3.4, DimensionUnit::Pt)),
606                Some(Dimension::new(5.6, DimensionUnit::Pt))
607            )
608        );
609        assert_eq!(input, "nope");
610    }
611
612    #[test]
613    fn dimension_or_braced_bare() {
614        let mut input = "1em rest";
615        let dim = lex::dimension_or_braced(&mut input).unwrap();
616        assert_eq!(dim, Dimension::new(1.0, DimensionUnit::Em));
617        assert_eq!(input, "rest");
618    }
619
620    #[test]
621    fn dimension_or_braced_with_braces() {
622        let mut input = "{1em} rest";
623        let dim = lex::dimension_or_braced(&mut input).unwrap();
624        assert_eq!(dim, Dimension::new(1.0, DimensionUnit::Em));
625        assert_eq!(input, " rest");
626    }
627
628    #[test]
629    fn glue_or_braced_with_braces() {
630        let mut input = "{1.2pt plus 3pt minus 1pt} rest";
631        let glue = lex::glue_or_braced(&mut input).unwrap();
632        assert_eq!(
633            glue,
634            (
635                Dimension::new(1.2, DimensionUnit::Pt),
636                Some(Dimension::new(3.0, DimensionUnit::Pt)),
637                Some(Dimension::new(1.0, DimensionUnit::Pt))
638            )
639        );
640        assert_eq!(input, " rest");
641    }
642
643    #[test]
644    fn numbers() {
645        let mut input = "123 -\"AEF24 --'3475 `\\a -.47";
646        assert_eq!(lex::integer(&mut input).unwrap(), 123);
647        assert_eq!(lex::integer(&mut input).unwrap(), -716580);
648        assert_eq!(lex::integer(&mut input).unwrap(), 1853);
649        assert_eq!(lex::integer(&mut input).unwrap(), 97);
650        assert_eq!(lex::floating_point(&mut input).unwrap(), -0.47);
651        assert_eq!(input, "");
652    }
653
654    #[test]
655    fn dvipsnames_colors() {
656        // Case-sensitive: dvipsnames `Apricot` returns the xcolor hex.
657        assert_eq!(lex::color("Apricot"), Some((0xFB, 0xB9, 0x82)));
658        assert_eq!(lex::color("Bittersweet"), Some((0xC0, 0x4F, 0x17)));
659        assert_eq!(lex::color("BlueGreen"), Some((0x00, 0xB3, 0xB8)));
660        assert_eq!(lex::color("WildStrawberry"), Some((0xEE, 0x29, 0x67)));
661        assert_eq!(lex::color("YellowOrange"), Some((0xFA, 0xA2, 0x1A)));
662
663        // `Blue` (dvipsnames) and `blue` (CSS) should resolve differently.
664        assert_eq!(lex::color("Blue"), Some((0x2D, 0x2F, 0x92)));
665        assert_eq!(lex::color("blue"), Some((0, 0, 255)));
666
667        // Hex passthrough still works.
668        assert_eq!(lex::color("#FBB982"), Some((0xFB, 0xB9, 0x82)));
669
670        // Unknown names still fail.
671        assert_eq!(lex::color("NotARealColor"), None);
672    }
673
674    #[test]
675    fn group_content() {
676        let mut input =
677            "this { { is a test } to see if { the content parsing { of this } } } works }";
678        let content = lex::group_content(&mut input, GroupingKind::Normal).unwrap();
679        assert_eq!(
680            content,
681            "this { { is a test } to see if { the content parsing { of this } } } works "
682        );
683    }
684}