Registry / RepositoryTypeScriptRustPython
Rollpie
ReadmeFiles
Versions
Info
Download
0.2.3555.5 KB2026-10-160.2.2572.8 KB2026-10-150.2.1580.5 KB2026-10-150.2.0579.9 KB2026-09-150.1.1576.6 KB2026-09-12
Version
0.2.2
Copyright
Rollpie, Irohabook
Publisher
math
Published
2026-10-15
Size
572.8 KB
Downloads
28
Checksum
ce26f898ba26eb49c71ebc68597f62f268b83e41bb265b1b5a01688966054588
Dependencies
None

token.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
//! Turns a LaTeX string into a sequence of tokens

use crate::error::Error;


/// A single token
#[derive(Debug, Clone, PartialEq)]
pub enum Token {
    /// A single letter, digit or symbol
    Character(char),
    /// The command name after a backslash (without the backslash)
    Command(String),
    /// The body of \text{...}, taken out as it is
    Text(String),
    LeftBrace,
    RightBrace,
    Caret,
    Underscore,
    /// &, the column separator in a matrix
    Ampersand,
    /// \\, the row separator in a matrix
    RowBreak,
}


/// A token and its position in the source string
#[derive(Debug, Clone, PartialEq)]
pub struct Located {
    pub token: Token,
    pub position: usize,
}


/// Turns a string into a sequence of tokens
pub fn tokenize(source: &str) -> Result<Vec<Located>, Error> {
    let characters: Vec<char> = source.chars().collect();
    let mut tokens = Vec::new();
    let mut index = 0;
    while index < characters.len() {
        let start = index;
        let character = characters[index];
        index += 1;
        let token = match character {
            c if c.is_whitespace() => continue,
            '{' => Token::LeftBrace,
            '}' => Token::RightBrace,
            '^' => Token::Caret,
            '_' => Token::Underscore,
            '&' => Token::Ampersand,
            '\\' => {
                let (token, next) = read_command(&characters, index, start)?;
                index = next;
                token
            }
            c => Token::Character(c),
        };
        tokens.push(Located {token, position: start});
    }
    Ok(tokens)
}


/// Reads from the character after a backslash. Returns the command token and the next position
fn read_command(characters: &[char], index: usize, start: usize) -> Result<(Token, usize), Error> {
    match characters.get(index) {
        None => Err(Error::new("no command name after the backslash", start)),
        Some('\\') => Ok((Token::RowBreak, index + 1)),
        Some(first) if first.is_ascii_alphabetic() => {
            let mut name = String::new();
            let mut next = index;
            while let Some(character) = characters.get(next) {
                if !character.is_ascii_alphabetic() {
                    break;
                }
                name.push(*character);
                next += 1;
            }
            if name == "text" {
                let (body, after) = read_text(characters, next, start)?;
                return Ok((Token::Text(body), after));
            }
            Ok((Token::Command(name), next))
        }
        Some(character) => Ok((Token::Command(character.to_string()), index + 1)),
    }
}


/// Reads the {text} after \text, keeping its spaces
fn read_text(characters: &[char], index: usize, start: usize) -> Result<(String, usize), Error> {
    let mut next = index;
    while matches!(characters.get(next), Some(character) if character.is_whitespace()) {
        next += 1;
    }
    if characters.get(next) != Some(&'{') {
        return Err(Error::new("\\text must be followed by {text}", start));
    }
    next += 1;
    let mut body = String::new();
    let mut depth = 1;
    while let Some(&character) = characters.get(next) {
        next += 1;
        match character {
            '{' => {
                depth += 1;
                body.push(character);
            }
            '}' => {
                depth -= 1;
                if depth == 0 {
                    return Ok((body, next));
                }
                body.push(character);
            }
            _ => body.push(character),
        }
    }
    Err(Error::new("\\text has an unclosed brace", start))
}


#[cfg(test)]
mod tests {
    use super::*;

    fn kinds(source: &str) -> Vec<Token> {
        tokenize(source).unwrap().into_iter().map(|located| located.token).collect()
    }

    #[test]
    fn reads_commands_and_characters() {
        assert_eq!(
            kinds(r"\frac{a}{2}"),
            vec![
                Token::Command("frac".to_string()),
                Token::LeftBrace,
                Token::Character('a'),
                Token::RightBrace,
                Token::LeftBrace,
                Token::Character('2'),
                Token::RightBrace,
            ]
        );
    }

    #[test]
    fn reads_row_break_and_ampersand() {
        assert_eq!(kinds(r"a & b \\ c"), vec![
            Token::Character('a'),
            Token::Ampersand,
            Token::Character('b'),
            Token::RowBreak,
            Token::Character('c'),
        ]);
    }

    #[test]
    fn keeps_spaces_inside_text() {
        assert_eq!(kinds(r"\text{a b}"), vec![Token::Text("a b".to_string())]);
        assert_eq!(kinds(r"\text {x}"), vec![Token::Text("x".to_string())]);
    }

    #[test]
    fn single_character_commands() {
        assert_eq!(kinds(r"\, \; \{"), vec![
            Token::Command(",".to_string()),
            Token::Command(";".to_string()),
            Token::Command("{".to_string()),
        ]);
    }

    #[test]
    fn records_positions() {
        let tokens = tokenize(r"x + \alpha").unwrap();
        assert_eq!(tokens[0].position, 0);
        assert_eq!(tokens[1].position, 2);
        assert_eq!(tokens[2].position, 4);
    }

    #[test]
    fn reports_a_lonely_backslash() {
        assert!(tokenize("a\\").is_err());
        assert!(tokenize(r"\text x").is_err());
    }
}