reifydb-rql 0.4.6

ReifyDB Query Language (RQL) parser and AST
Documentation
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2025 ReifyDB

use crate::token::{
	cursor::Cursor,
	token::{Literal::Text, Token, TokenKind},
};

/// Scan for a text literal ('...' or "...")
pub fn scan_text<'b>(cursor: &mut Cursor<'b>) -> Option<Token<'b>> {
	let quote = cursor.peek()?;
	if quote != '\'' && quote != '"' {
		return None;
	}

	let start_pos = cursor.pos();
	let start_line = cursor.line();
	let start_column = cursor.column();

	cursor.consume(); // consume opening quote

	let text_start = cursor.pos();

	while let Some(ch) = cursor.peek() {
		if ch == quote {
			let text_end = cursor.pos();
			cursor.consume(); // consume closing quote

			return Some(Token {
				kind: TokenKind::Literal(Text),
				fragment: cursor.make_utf8_fragment(
					text_start,
					text_end,
					start_line,
					start_column + 1, /* +1 for opening
					                   * quote */
					start_pos,
				),
			});
		}

		cursor.consume();
	}

	None // Unterminated string
}

#[cfg(test)]
pub mod tests {
	use super::*;
	use crate::{
		bump::Bump,
		token::{token::Literal::Number, tokenize},
	};

	#[test]
	fn test_text_single_quotes() {
		let bump = Bump::new();
		let tokens = tokenize(&bump, "'hello'").unwrap();
		assert_eq!(tokens[0].kind, TokenKind::Literal(Text));
		assert_eq!(tokens[0].fragment.text(), "hello");
	}

	#[test]
	fn test_text_double_quotes() {
		let bump = Bump::new();
		let tokens = tokenize(&bump, "\"hello\"").unwrap();
		assert_eq!(tokens[0].kind, TokenKind::Literal(Text));
		assert_eq!(tokens[0].fragment.text(), "hello");
	}

	#[test]
	fn test_text_single_quotes_with_double_inside() {
		let bump = Bump::new();
		let tokens = tokenize(&bump, "'some text\"xx\"no problem'").unwrap();
		assert_eq!(tokens[0].kind, TokenKind::Literal(Text));
		assert_eq!(tokens[0].fragment.text(), "some text\"xx\"no problem");
	}

	#[test]
	fn test_text_double_quotes_with_single_inside() {
		let bump = Bump::new();
		let tokens = tokenize(&bump, "\"some text'xx'no problem\"").unwrap();
		assert_eq!(tokens[0].kind, TokenKind::Literal(Text));
		assert_eq!(tokens[0].fragment.text(), "some text'xx'no problem");
	}

	#[test]
	fn test_text_with_trailing() {
		let bump = Bump::new();
		let tokens = tokenize(&bump, "'data' 123").unwrap();
		assert_eq!(tokens[0].fragment.text(), "data");
		assert_eq!(tokens[1].kind, TokenKind::Literal(Number));
		assert_eq!(tokens[1].fragment.text(), "123");
	}

	#[test]
	fn test_text_double_quotes_with_trailing() {
		let bump = Bump::new();
		let tokens = tokenize(&bump, "\"data\" 123").unwrap();
		assert_eq!(tokens[0].fragment.text(), "data");
		assert_eq!(tokens[1].kind, TokenKind::Literal(Number));
		assert_eq!(tokens[1].fragment.text(), "123");
	}

	#[test]
	fn test_text_single_unterminated_fails() {
		let bump = Bump::new();
		let tokens = tokenize(&bump, "'not closed");
		// Should fail or return no text token
		assert!(tokens.is_err() || tokens.unwrap().iter().all(|t| !matches!(t.kind, TokenKind::Literal(Text))));
	}

	#[test]
	fn test_text_double_unterminated_fails() {
		let bump = Bump::new();
		let tokens = tokenize(&bump, "\"not closed");
		// Should fail or return no text token
		assert!(tokens.is_err() || tokens.unwrap().iter().all(|t| !matches!(t.kind, TokenKind::Literal(Text))));
	}

	#[test]
	fn test_text_empty_single_quotes() {
		let bump = Bump::new();
		let tokens = tokenize(&bump, "''").unwrap();
		assert_eq!(tokens[0].kind, TokenKind::Literal(Text));
		assert_eq!(tokens[0].fragment.text(), "");
	}

	#[test]
	fn test_text_empty_double_quotes() {
		let bump = Bump::new();
		let tokens = tokenize(&bump, "\"\"").unwrap();
		assert_eq!(tokens[0].kind, TokenKind::Literal(Text));
		assert_eq!(tokens[0].fragment.text(), "");
	}

	#[test]
	fn test_text_mixed_quotes_comptokenize() {
		let bump = Bump::new();
		let tokens = tokenize(&bump, "'He said \"Hello\" and she replied \"Hi\"'").unwrap();
		assert_eq!(tokens[0].kind, TokenKind::Literal(Text));
		assert_eq!(tokens[0].fragment.text(), "He said \"Hello\" and she replied \"Hi\"");
	}

	#[test]
	fn test_text_multiple_nested_quotes() {
		let bump = Bump::new();
		let tokens = tokenize(&bump, "\"It's a 'nice' day, isn't it?\"").unwrap();
		assert_eq!(tokens[0].kind, TokenKind::Literal(Text));
		assert_eq!(tokens[0].fragment.text(), "It's a 'nice' day, isn't it?");
	}
}