use crate::{ValveKeyValue, ValveKeyValueType};
use crate::error::{Error, Result};
#[derive(Clone, Debug)]
pub struct TrackedChars<'a> {
inner: std::iter::Peekable<std::str::Chars<'a>>,
line: usize,
col: usize,
}
impl<'a> TrackedChars<'a> {
pub fn new(input: &'a str) -> Self {
Self { inner: input.chars().peekable(), line: 0, col: 0 }
}
pub fn get_pos(&self) -> (usize, usize) {
(self.line, self.col)
}
#[allow(clippy::should_implement_trait)]
pub fn next(&mut self) -> Option<char> {
let ch = self.inner.next();
if let Some(c) = ch {
match c {
'\n' => {
self.line += 1;
self.col = 0;
},
'\r' => self.col = 0,
_ => self.col += 1
}
}
ch
}
pub fn peek(&mut self) -> Option<&char> {
self.inner.peek()
}
pub fn peek_2(&mut self) -> Option<char> {
let mut next = self.inner.clone();
next.next()?;
next.next()
}
}
pub fn parse_string( input: &mut TrackedChars,
use_escape_sequences: bool
) -> Result<String> {
let mut output_string = String::new();
let is_quote = Some(&'"') == input.peek();
if is_quote {
input.next();
}
let is_end = |inp: char| -> bool {
if is_quote {
inp == '"'
} else {
inp.is_whitespace() || inp == '{' || inp == '}' || inp == '"'
}
};
while let Some(ch) = input.peek() {
if use_escape_sequences {
match ch {
'\\' => match input.peek_2() {
Some(ch) => {
match ch {
'\\' => {
output_string.push('\\');
input.next();
input.next();
},
'"' => {
output_string.push('"');
input.next();
input.next();
},
'n' => {
output_string.push('\n');
input.next();
input.next();
},
't' => {
output_string.push('\t');
input.next();
input.next();
},
_ => {input.next();}
}
},
None => return Err(Error::UnexpectedEndOfFile)
},
_ => if is_end(*ch) {
if *ch == '"' && is_quote {input.next();}
return Ok(output_string);
} else {
output_string.push(*ch);
input.next();
}
}
} else {
if is_end(*ch) {
if *ch == '"' && is_quote {input.next();}
return Ok(output_string);
} else {
output_string.push(*ch);
input.next();
}
}
}
if is_quote {
Err(Error::UnexpectedEndOfFile)
} else {
Ok(output_string)
}
}
pub fn skip_comments(input: &mut TrackedChars) {
while let Some(ch) = input.peek() {
if ch == &'\n' {
return;
} else {
input.next();
}
}
}
pub fn parse_object(
input: &mut TrackedChars,
use_escape_sequences: bool,
depth: usize
) -> Result<Vec<ValveKeyValue>> {
let mut keypairs: Vec<ValveKeyValue> = Vec::new();
let mut current_key: Option<String> = None;
while let Some(ch) = input.peek() {
if ch == &'{' {
let pos = input.get_pos();
input.next();
let parsed_object = parse_object(input, use_escape_sequences, depth + 1)?;
if let Some(curr_key) = ¤t_key {
keypairs.push(ValveKeyValue::new(curr_key.clone(), ValveKeyValueType::Object(parsed_object)));
current_key = None;
} else {
return Err(Error::UnexpectedObjectAsKey { line: pos.0, col: pos.1 });
}
} else if ch == &'}' {
let pos = input.get_pos();
if depth == 0 {
return Err(Error::UnexpectedEndOfObject { line: pos.0, col: pos.1 });
}
if current_key.is_some() {
return Err(Error::MissingValue { line: pos.0, col: pos.1 });
}
input.next();
return Ok(keypairs);
} else if ch.is_whitespace() {
input.next();
} else if ch == &'/' && input.peek_2() == Some('/') {
input.next();
skip_comments(input);
} else {
let parsed_string = parse_string(input, use_escape_sequences)?;
if let Some(curr_key) = ¤t_key {
keypairs.push(ValveKeyValue::new(curr_key.clone(), ValveKeyValueType::String(parsed_string)));
current_key = None;
} else {
current_key = Some(parsed_string);
}
}
}
if current_key.is_some() {
let pos = input.get_pos();
Err(Error::MissingValue { line: pos.0, col: pos.1 })
} else if depth == 0 {
Ok(keypairs)
} else {
Err(Error::UnexpectedEndOfFile)
}
}
pub fn parse(input: String, use_escape_sequences: bool) -> Result<Vec<ValveKeyValue>> {
parse_object(&mut TrackedChars::new(&input), use_escape_sequences, 0)
}