Registry / RepositoryTypeScriptRustPython
Rollpie
ReadmeFiles
Versions
Info
Download
0.2.0579.9 KB2026-09-150.1.1576.6 KB2026-09-12
Version
0.1.1
Copyright
Rollpie, Irohabook
Publisher
math
Published
2026-09-12
Size
576.6 KB
Downloads
4
Checksum
8e0181e42f4abdc9f91c1ff48b058761bf1940be8d2d4747e5e74bd3014f4322
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
//! LaTeX の文字列をトークンの並びにする

use crate::error::Error;


/// 1 つのトークン
#[derive(Debug, Clone, PartialEq)]
pub enum Token {
    /// 英数字や記号 1 文字
    Character(char),
    /// バックスラッシュに続くコマンド名 (バックスラッシュは含まない)
    Command(String),
    /// \text{...} の中身をそのまま取り出したもの
    Text(String),
    LeftBrace,
    RightBrace,
    Caret,
    Underscore,
    /// & 行列の列区切り
    Ampersand,
    /// \\ 行列の行区切り
    RowBreak,
}


/// トークンと、元の文字列でのその位置
#[derive(Debug, Clone, PartialEq)]
pub struct Located {
    pub token: Token,
    pub position: usize,
}


/// 文字列をトークンの並びにする
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)
}


/// バックスラッシュの次の文字から読み始めて、コマンドのトークンと次の位置を返す
fn read_command(characters: &[char], index: usize, start: usize) -> Result<(Token, usize), Error> {
    match characters.get(index) {
        None => Err(Error::new("バックスラッシュの後にコマンド名がありません", 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)),
    }
}


/// \text の後の {文字列} を、空白を残したまま読み取る
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 の後には {文字列} が必要です", 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 の波括弧が閉じていません", 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());
    }
}