aboutsummaryrefslogtreecommitdiff
path: root/rprt-engine/src/token.rs
diff options
context:
space:
mode:
Diffstat (limited to 'rprt-engine/src/token.rs')
-rw-r--r--rprt-engine/src/token.rs279
1 files changed, 279 insertions, 0 deletions
diff --git a/rprt-engine/src/token.rs b/rprt-engine/src/token.rs
new file mode 100644
index 0000000..cde8164
--- /dev/null
+++ b/rprt-engine/src/token.rs
@@ -0,0 +1,279 @@
+use logos::{Lexer, Logos};
+
+fn lex_delimited(
+ lex: &mut Lexer<Token>,
+ delimiter: char,
+ ignore_inside: Option<&[(char, char)]>,
+) -> Option<String> {
+ let remainder = lex.remainder();
+ let mut chars = remainder.chars();
+ let mut escaped = false;
+ let mut pos = 0;
+ let mut looking_for: Option<char> = None;
+
+ while let Some(c) = chars.next() {
+ pos += c.len_utf8();
+
+ // escaping
+ if escaped {
+ escaped = false;
+ continue;
+ }
+ if c == '\\' {
+ escaped = true;
+ continue;
+ }
+
+ // capture in
+ if let Some(end) = looking_for {
+ if c == end {
+ looking_for = None;
+ }
+ continue;
+ }
+ if let Some(ignore_inside) = ignore_inside {
+ for &(start, end) in ignore_inside {
+ if c == start {
+ looking_for = Some(end);
+ continue;
+ }
+ }
+ }
+
+ if c == delimiter {
+ let content = &remainder[..pos - 1];
+ lex.bump(pos);
+ return Some(content.to_string());
+ }
+ }
+
+ None
+}
+
+fn lex_regex(lex: &mut Lexer<Token>) -> Option<String> {
+ let character_class = vec![('[', ']')];
+ lex_delimited(lex, '/', Some(&character_class))
+}
+
+fn lex_text(lex: &mut Lexer<Token>) -> Option<String> {
+ lex_delimited(lex, '"', None)
+}
+
+#[derive(Debug, Clone, PartialEq, Logos)]
+#[logos(skip r"\s+")]
+pub enum Token {
+ // Literals
+ #[token("\"", lex_text)]
+ TextLiteral(String),
+
+ #[regex(r"[0-9]+", |lex| lex.slice().parse().ok())]
+ Number(usize),
+
+ #[token("/", lex_regex)]
+ Regex(String),
+
+ // Selection Functions
+ #[token("e")]
+ Empty,
+
+ #[token("$")]
+ EndOfBuffer,
+
+ #[token("#")]
+ CharacterOffset,
+
+ #[token("-")]
+ Span,
+
+ #[token("x")]
+ AllMatches,
+
+ #[token("+")]
+ PositiveLineOffset,
+
+ #[token("B")]
+ BufferMatch,
+
+ // Text Functions
+ #[token(".")]
+ Dot,
+
+ #[token("c")]
+ Change,
+
+ #[token("i")]
+ Insert,
+
+ #[token("a")]
+ Append,
+
+ #[token("d")]
+ Delete,
+
+ #[token("w")]
+ Write,
+
+ #[token("|")]
+ Pipe,
+
+ // Selection Function Operators
+ #[token("'")]
+ Reverse,
+
+ #[token(";")]
+ Sequential,
+
+ #[token("~")]
+ Complement,
+
+ #[token("?")]
+ Question,
+
+ // Text Function Operators
+ #[token("@")]
+ Swap,
+
+ // Composition/Combinators
+ #[token(">")]
+ Before,
+
+ #[token("<")]
+ After,
+
+ // Grouping
+ #[token("(")]
+ LParen,
+
+ #[token(")")]
+ RParen,
+
+ #[token("{")]
+ LBrace,
+
+ #[token("}")]
+ RBrace,
+
+ // Statement/Buffer control
+ #[token("b")]
+ BufferSwitch,
+
+ // Separators
+ #[token(",")]
+ Comma,
+}
+
+pub fn tokenize(input: &str) -> Result<Vec<Token>, String> {
+ let mut tokens = Vec::new();
+ let mut lexer = Token::lexer(input);
+
+ while let Some(result) = lexer.next() {
+ match result {
+ Ok(token) => tokens.push(token),
+ Err(()) => {
+ return Err(format!(
+ "Unexpected character at position {}: '{}'",
+ lexer.span().start,
+ lexer.slice()
+ ));
+ }
+ }
+ }
+
+ Ok(tokens)
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::token::*;
+
+ #[test]
+ fn test_delimited_literals_with_escapes() {
+ assert_eq!(
+ tokenize(r#""" "hello" "escaped\"quote" "backslash\\" "mixed\n\t""#),
+ Ok(vec![
+ Token::TextLiteral("".to_string()),
+ Token::TextLiteral("hello".to_string()),
+ Token::TextLiteral(r#"escaped\"quote"#.to_string()),
+ Token::TextLiteral(r#"backslash\\"#.to_string()),
+ Token::TextLiteral(r#"mixed\n\t"#.to_string()),
+ ])
+ );
+
+ assert_eq!(
+ tokenize(r#"// /[a-z]+/ /foo\/bar/ /[/]/ /(a|b)\d+/ /\\/"#),
+ Ok(vec![
+ Token::Regex("".to_string()),
+ Token::Regex("[a-z]+".to_string()),
+ Token::Regex(r#"foo\/bar"#.to_string()),
+ Token::Regex("[/]".to_string()),
+ Token::Regex(r#"(a|b)\d+"#.to_string()),
+ Token::Regex(r#"\\"#.to_string()),
+ ])
+ );
+ }
+
+ #[test]
+ fn test_realistic_expressions() {
+ assert_eq!(
+ tokenize(r#"/pattern/ c "replacement""#),
+ Ok(vec![
+ Token::Regex("pattern".to_string()),
+ Token::Change,
+ Token::TextLiteral("replacement".to_string())
+ ])
+ );
+
+ assert_eq!(
+ tokenize(r#"B/*.txt/ ?'/\w+/ ;#42"#),
+ Ok(vec![
+ Token::BufferMatch,
+ Token::Regex(r#"*.txt"#.to_string()),
+ Token::Question,
+ Token::Reverse,
+ Token::Regex(r#"\w+"#.to_string()),
+ Token::Sequential,
+ Token::CharacterOffset,
+ Token::Number(42)
+ ])
+ );
+
+ assert_eq!(
+ tokenize(r#"{ /a/ @i, /b/ c<(|<"sort") }"#),
+ Ok(vec![
+ Token::LBrace,
+ Token::Regex("a".to_string()),
+ Token::Swap,
+ Token::Insert,
+ Token::Comma,
+ Token::Regex("b".to_string()),
+ Token::Change,
+ Token::After,
+ Token::LParen,
+ Token::Pipe,
+ Token::After,
+ Token::TextLiteral("sort".to_string()),
+ Token::RParen,
+ Token::RBrace
+ ])
+ );
+
+ assert_eq!(
+ tokenize(r#"~?';x/pattern/"#),
+ Ok(vec![
+ Token::Complement,
+ Token::Question,
+ Token::Reverse,
+ Token::Sequential,
+ Token::AllMatches,
+ Token::Regex("pattern".to_string())
+ ])
+ );
+ }
+
+ #[test]
+ fn test_errors() {
+ assert!(tokenize(r#""unterminated"#).is_err());
+ assert!(tokenize(r#"/unterminated"#).is_err());
+ assert!(tokenize(r#""escaped at end\"#).is_err());
+ }
+}