use std::io::{Error, ErrorKind};
use std::path::Path;
pub struct Source {
pub input: String,
pub pos: usize,
pub line: usize,
pub col: usize,
}
impl Source {
#[allow(dead_code)]
pub fn new(file_path: &str) -> std::io::Result<Self> {
if Path::new(file_path).exists() {
let input = std::fs::read_to_string(file_path)?;
return Ok(Self {
input,
pos: 0,
line: 1,
col: 1,
});
}
Err(Error::new(ErrorKind::NotFound, "File does not exist"))
}
pub fn from_string(input: String) -> Self {
Self {
input,
pos: 0,
line: 1,
col: 1,
}
}
#[allow(dead_code)]
pub fn from_test_str(string: &str) -> Source {
Source::from_string(string.to_string())
}
pub fn next_char(&mut self) -> Option<char> {
if self.pos >= self.input.len() {
return None;
}
let ch = self.input[self.pos..].chars().next()?;
let char_len = ch.len_utf8();
self.pos += char_len;
if ch == '\n' {
self.line += 1;
self.col = 1;
} else {
self.col += unicode_width::UnicodeWidthChar::width(ch).unwrap_or(1);
}
Some(ch)
}
pub fn peek(&self) -> Option<char> {
if self.pos >= self.input.len() {
return None;
}
self.input[self.pos..].chars().next()
}
pub fn peek_nth(&self, n: usize) -> Option<char> {
if self.pos >= self.input.len() {
return None;
}
self.input[self.pos..].chars().nth(n)
}
pub fn consume_until(&mut self, stop: char) -> String {
let mut buf = String::new();
while let Some(c) = self.peek() {
if c == stop {
break;
}
buf.push(c);
self.next_char();
}
buf.trim().to_string()
}
pub fn consume_multiline_comment(&mut self) -> (String, bool) {
let mut buf = String::new();
let mut found = false;
while let Some(c) = self.next_char() {
if c == '*' && self.peek() == Some('/') {
self.next_char(); found = true;
break;
}
buf.push(c);
}
(buf.trim().to_string(), found)
}
}
#[cfg(test)]
mod tests {
use super::Source;
#[test]
fn test_next_char_basic() {
let mut src = Source {
input: "abc\n".to_string(),
pos: 0,
line: 1,
col: 1,
};
assert_eq!(src.next_char(), Some('a'));
assert_eq!(src.line, 1);
assert_eq!(src.col, 2);
assert_eq!(src.next_char(), Some('b'));
assert_eq!(src.line, 1);
assert_eq!(src.col, 3);
assert_eq!(src.next_char(), Some('c'));
assert_eq!(src.line, 1);
assert_eq!(src.col, 4);
assert_eq!(src.next_char(), Some('\n'));
assert_eq!(src.line, 2);
assert_eq!(src.col, 1);
assert_eq!(src.next_char(), None);
}
#[test]
fn test_peek() {
let mut src = Source {
input: "xy".to_string(),
pos: 0,
line: 1,
col: 1,
};
assert_eq!(src.peek(), Some('x')); assert_eq!(src.line, 1);
assert_eq!(src.col, 1);
assert_eq!(src.next_char(), Some('x'));
assert_eq!(src.line, 1);
assert_eq!(src.col, 2);
assert_eq!(src.peek(), Some('y'));
assert_eq!(src.line, 1);
assert_eq!(src.col, 2);
assert_eq!(src.next_char(), Some('y'));
assert_eq!(src.line, 1);
assert_eq!(src.col, 3);
assert_eq!(src.peek(), None);
}
#[test]
fn test_multiline_positioning() {
let mut src = Source::from_test_str("hello\nworld\n");
for expected_char in "hello".chars() {
assert_eq!(src.next_char(), Some(expected_char));
}
assert_eq!(src.line, 1);
assert_eq!(src.col, 6);
assert_eq!(src.next_char(), Some('\n')); assert_eq!(src.line, 2);
assert_eq!(src.col, 1);
for expected_char in "world".chars() {
assert_eq!(src.next_char(), Some(expected_char));
}
assert_eq!(src.line, 2);
assert_eq!(src.col, 6);
}
#[test]
fn test_consume_until() {
let mut src = Source::from_test_str("hello world");
let result = src.consume_until(' ');
assert_eq!(result, "hello");
assert_eq!(src.line, 1);
assert_eq!(src.col, 6); assert_eq!(src.next_char(), Some(' ')); assert_eq!(src.next_char(), Some('w')); }
#[test]
fn test_consume_multiline_comment() {
let mut src = Source::from_test_str(" hello\nworld*/");
let (result, found) = src.consume_multiline_comment();
assert_eq!(result, "hello\nworld");
assert!(found);
assert_eq!(src.line, 2); assert_eq!(src.col, 8); }
#[test]
fn test_unicode_handling() {
let mut src = Source::from_test_str("こんにちは");
let ch = src.next_char();
println!("First char: {:?}, col: {}", ch, src.col);
assert_eq!(ch, Some('こ'));
assert_eq!(src.col, 3);
let ch = src.next_char();
println!("Second char: {:?}, col: {}", ch, src.col);
assert_eq!(ch, Some('ん'));
assert_eq!(src.col, 5);
let ch = src.next_char();
println!("Third char: {:?}, col: {}", ch, src.col);
assert_eq!(ch, Some('に'));
assert_eq!(src.col, 7);
let ch = src.next_char();
println!("Fourth char: {:?}, col: {}", ch, src.col);
assert_eq!(ch, Some('ち'));
assert_eq!(src.col, 9);
let ch = src.next_char();
println!("Fifth char: {:?}, col: {}", ch, src.col);
assert_eq!(ch, Some('は'));
assert_eq!(src.col, 11);
let mut src = Source::from_test_str("hello 🌟");
for _ in 0..6 {
src.next_char();
}
assert_eq!(src.col, 7); assert_eq!(src.next_char(), Some('🌟'));
assert_eq!(src.col, 9); }
#[test]
fn test_from_test_str_type_consistency() {
let src = Source::from_test_str("test");
assert_eq!(src.input, "test".to_string());
}
}