use crate::error::{Error, Result};
use crate::value::Value;
use std::collections::BTreeMap;
pub fn parse(source: &str) -> Result<Value> {
let mut parser = PropertiesParser::new(source.to_string());
parser.parse()
}
pub struct PropertiesParser {
input: String,
position: usize,
line: usize,
column: usize,
}
impl PropertiesParser {
pub fn new(input: String) -> Self {
Self {
input,
position: 0,
line: 1,
column: 1,
}
}
pub fn parse(&mut self) -> Result<Value> {
let mut properties = BTreeMap::new();
while !self.at_end() {
self.skip_whitespace_and_comments();
if self.at_end() {
break;
}
let (key, value) = self.parse_property()?;
properties.insert(key, value);
}
Ok(Value::table(properties))
}
fn parse_property(&mut self) -> Result<(String, Value)> {
let key = self.parse_key()?;
self.skip_whitespace();
if self.current_char() != '=' && self.current_char() != ':' {
return Err(Error::Parse {
message: format!("Expected '=' or ':', found '{}'", self.current_char()),
line: self.line,
column: self.column,
file: None,
});
}
self.advance(); self.skip_whitespace();
let value = self.parse_value()?;
Ok((key, value))
}
fn parse_key(&mut self) -> Result<String> {
let mut key = String::new();
while !self.at_end() {
let ch = self.current_char();
match ch {
'=' | ':' => break,
'\\' => {
self.advance();
if self.at_end() {
return Err(Error::Parse {
message: "Unexpected end of input in key".to_string(),
line: self.line,
column: self.column,
file: None,
});
}
let escaped = self.parse_escape()?;
key.push_str(&escaped);
}
'\n' | '\r' => {
return Err(Error::Parse {
message: "Unexpected newline in key".to_string(),
line: self.line,
column: self.column,
file: None,
});
}
_ => {
key.push(ch);
self.advance();
}
}
}
if key.trim().is_empty() {
return Err(Error::Parse {
message: "Empty key name".to_string(),
line: self.line,
column: self.column,
file: None,
});
}
Ok(key.trim().to_string())
}
fn parse_value(&mut self) -> Result<Value> {
let mut value = String::new();
while !self.at_end() {
let ch = self.current_char();
match ch {
'\\' => {
self.advance();
if self.at_end() {
break;
}
if self.current_char() == '\n' || self.current_char() == '\r' {
self.skip_newline();
self.skip_whitespace();
continue;
}
let escaped = self.parse_escape()?;
value.push_str(&escaped);
}
'\n' | '\r' => break,
_ => {
value.push(ch);
self.advance();
}
}
}
let trimmed = value.trim();
Ok(self.infer_value_type(trimmed))
}
fn parse_escape(&mut self) -> Result<String> {
let ch = self.current_char();
self.advance();
match ch {
'n' => Ok("\n".to_string()),
't' => Ok("\t".to_string()),
'r' => Ok("\r".to_string()),
'\\' => Ok("\\".to_string()),
'=' => Ok("=".to_string()),
':' => Ok(":".to_string()),
' ' => Ok(" ".to_string()),
'u' => self.parse_unicode_escape(),
_ => Ok(ch.to_string()),
}
}
fn parse_unicode_escape(&mut self) -> Result<String> {
let mut hex_digits = String::new();
for _ in 0..4 {
if self.at_end() {
return Err(Error::Parse {
message: "Incomplete unicode escape".to_string(),
line: self.line,
column: self.column,
file: None,
});
}
let ch = self.current_char();
if ch.is_ascii_hexdigit() {
hex_digits.push(ch);
self.advance();
} else {
return Err(Error::Parse {
message: format!("Invalid hex digit in unicode escape: '{ch}'"),
line: self.line,
column: self.column,
file: None,
});
}
}
let code_point = match u32::from_str_radix(&hex_digits, 16) {
Ok(cp) => cp,
Err(_) => {
return Err(Error::Parse {
message: format!("Invalid hex digits in unicode escape: {hex_digits}"),
line: self.line,
column: self.column,
file: None,
});
}
};
if let Some(unicode_char) = char::from_u32(code_point) {
Ok(unicode_char.to_string())
} else {
Err(Error::Parse {
message: format!("Invalid unicode code point: {code_point}"),
line: self.line,
column: self.column,
file: None,
})
}
}
fn infer_value_type(&self, value: &str) -> Value {
if value.is_empty() {
return Value::string(String::new());
}
match value.to_lowercase().as_str() {
"true" => return Value::bool(true),
"false" => return Value::bool(false),
_ => {}
}
if let Ok(int_val) = value.parse::<i64>() {
return Value::integer(int_val);
}
if let Ok(float_val) = value.parse::<f64>() {
return Value::float(float_val);
}
Value::string(value.to_string())
}
fn skip_whitespace_and_comments(&mut self) {
while !self.at_end() {
match self.current_char() {
' ' | '\t' => self.advance(),
'\n' | '\r' => self.skip_newline(),
'#' | '!' => self.skip_comment(),
_ => break,
}
}
}
fn skip_whitespace(&mut self) {
while !self.at_end() && (self.current_char() == ' ' || self.current_char() == '\t') {
self.advance();
}
}
fn skip_comment(&mut self) {
while !self.at_end() && self.current_char() != '\n' && self.current_char() != '\r' {
self.advance();
}
}
fn skip_newline(&mut self) {
if !self.at_end() && self.current_char() == '\r' {
self.advance();
}
if !self.at_end() && self.current_char() == '\n' {
self.advance();
}
}
fn current_char(&self) -> char {
self.input.chars().nth(self.position).unwrap_or('\0')
}
fn at_end(&self) -> bool {
self.position >= self.input.len()
}
fn advance(&mut self) {
if !self.at_end() {
if self.current_char() == '\n' {
self.line += 1;
self.column = 1;
} else {
self.column += 1;
}
self.position += 1;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_simple_properties() {
let input = "key1=value1\nkey2=123\nbool_key=true";
let mut parser = PropertiesParser::new(input.to_string());
let result = parser.parse().unwrap();
if let Value::Table(table) = result {
assert_eq!(table.get("key1").unwrap().as_string().unwrap(), "value1");
assert_eq!(table.get("key2").unwrap().as_integer().unwrap(), 123);
assert!(table.get("bool_key").unwrap().as_bool().unwrap());
}
}
#[test]
fn test_comments() {
let input = "# This is a comment\nkey1=value1\n! Another comment\nkey2=value2";
let mut parser = PropertiesParser::new(input.to_string());
let result = parser.parse().unwrap();
if let Value::Table(table) = result {
assert_eq!(table.get("key1").unwrap().as_string().unwrap(), "value1");
assert_eq!(table.get("key2").unwrap().as_string().unwrap(), "value2");
}
}
#[test]
fn test_escape_sequences() {
let input = r"key1=line1\nline2\ttab";
let mut parser = PropertiesParser::new(input.to_string());
let result = parser.parse().unwrap();
if let Value::Table(table) = result {
assert_eq!(
table.get("key1").unwrap().as_string().unwrap(),
"line1\nline2\ttab"
);
}
}
#[test]
fn test_colon_separator() {
let input = "key1:value1\nkey2: value2";
let mut parser = PropertiesParser::new(input.to_string());
let result = parser.parse().unwrap();
if let Value::Table(table) = result {
assert_eq!(table.get("key1").unwrap().as_string().unwrap(), "value1");
assert_eq!(table.get("key2").unwrap().as_string().unwrap(), "value2");
}
}
}