use std::io::Error as IoError;
use strip_bom::StripBom;
use regex::Regex;
use crate::model::game::{
MapDataBuilder,
TitleTier,
TitleId,
ProvinceId
};
pub use error::{
LandedTitlesReadingError,
LandedTitlesParsingError,
};
const COMMENT_MARKER: char = '#';
pub fn read_landed_titles_from_file_lines(
map_data_builder: &mut MapDataBuilder,
lines: impl IntoIterator<Item = Result<String, IoError>>
) -> Result<(), LandedTitlesReadingError> {
let mut parser_state = ParserState::LeftHandSideStart;
let mut blocks_stack = Vec::with_capacity(TitleTier::TIERS_COUNT + 1);
let mut province_id = None;
let mut line_number = 0;
for line in lines.into_iter() {
line_number += 1;
for token in tokenize_line(&preprocess_line(line?).strip_bom()) {
macro_rules! unexpected_token_err {
() => {
{ return Err(LandedTitlesParsingError::from_unexpected_token(token.content, line_number).into()); }
};
}
if token.kind == TokenKind::Comment {
continue;
}
parser_state = match parser_state {
ParserState::LeftHandSideStart => {
match token.kind {
TokenKind::CodeItem => {
ParserState::LeftHandSideAfter(
if matches!(blocks_stack.last(), Some(BlockKind::TitleBodyBlock(_))) && token.content == "province" {
LeftHandSideKind::ProvinceKeyword
} else if let Ok(title_id) = TitleId::try_from(token.content) {
LeftHandSideKind::TitleId(title_id)
} else {
LeftHandSideKind::Other
}
)
}
TokenKind::ClosingBrace => {
match blocks_stack.last() {
Some(block_kind) => {
if let BlockKind::TitleBodyBlock(title_id) = block_kind {
let liege_id = blocks_stack.iter()
.rev()
.skip(1)
.find_map(|block| {
if let BlockKind::TitleBodyBlock(title_id) = block {
Some(title_id)
} else {
None
}
});
map_data_builder.add_title(title_id.clone(), liege_id.cloned(), province_id.take());
}
blocks_stack.pop();
ParserState::LeftHandSideStart
},
_ => unexpected_token_err!(),
}
}
_ => unexpected_token_err!()
}
},
ParserState::LeftHandSideAfter(lhs_kind) => {
match lhs_kind {
LeftHandSideKind::ProvinceKeyword | LeftHandSideKind::TitleId(_) => match token.kind {
TokenKind::KeyValueSeparator => ParserState::RightHandSideAfter(lhs_kind),
_ => unexpected_token_err!()
}
LeftHandSideKind::Other => match token.kind {
TokenKind::KeyValueSeparator => ParserState::RightHandSideAfter(lhs_kind),
TokenKind::CodeItem => ParserState::CodeItemList,
_ => unexpected_token_err!()
}
}
},
ParserState::RightHandSideAfter(lhs_kind) => {
match lhs_kind {
LeftHandSideKind::TitleId(title_id) => match token.kind {
TokenKind::OpeningBrace => {
blocks_stack.push(BlockKind::TitleBodyBlock(title_id));
ParserState::LeftHandSideStart
}
_ => unexpected_token_err!()
},
LeftHandSideKind::ProvinceKeyword => match token.kind {
TokenKind::CodeItem => match token.content.parse::<u32>() {
Ok(province_id_int) => {
assert!(matches!(blocks_stack.last(), Some(BlockKind::TitleBodyBlock(_))));
province_id.replace(ProvinceId(province_id_int));
ParserState::LeftHandSideStart
},
Err(err) => { return Err(LandedTitlesParsingError::from_malformed_province_id(token.content, line_number, err).into()); },
}
_ => unexpected_token_err!()
},
LeftHandSideKind::Other => match token.kind {
TokenKind::CodeItem => ParserState::LeftHandSideStart,
TokenKind::OpeningBrace => {
blocks_stack.push(BlockKind::OtherBlock);
ParserState::LeftHandSideStart
},
_ => unexpected_token_err!()
}
}
},
ParserState::CodeItemList => match token.kind {
TokenKind::CodeItem => ParserState::CodeItemList,
TokenKind::ClosingBrace => {
assert!(matches!(blocks_stack.last(), Some(BlockKind::OtherBlock)));
blocks_stack.pop();
ParserState::LeftHandSideStart
},
_ => unexpected_token_err!()
}
}
}
}
Ok(())
}
struct Token<'a> {
content: &'a str,
kind: TokenKind
}
impl<'a> Token<'a> {
pub fn from_noncomment(content: &'a str) -> Option<Self> {
if !content.is_empty() {
Some(Self{
content,
kind: recognize_noncomment_token_kind(content)
})
} else {
None
}
}
pub fn from_comment(content: &'a str) -> Self {
Self{
content,
kind: TokenKind::Comment
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum TokenKind {
Comment,
CodeItem,
KeyValueSeparator,
OpeningBrace,
ClosingBrace
}
enum ParserState {
LeftHandSideStart,
LeftHandSideAfter(LeftHandSideKind),
RightHandSideAfter(LeftHandSideKind),
CodeItemList
}
enum LeftHandSideKind {
TitleId(TitleId),
ProvinceKeyword,
Other
}
enum BlockKind {
TitleBodyBlock(TitleId),
OtherBlock
}
fn preprocess_line(mut line: String) -> String {
line = line.replace('{', " { ")
.replace('}', " } ");
let key_value_separator_regex = Regex::new(r"[!<>]?=").expect("key_value_separator_regex must be correct");
line = key_value_separator_regex.replace(&line, "=").to_string();
if !line.contains("hsv") && !line.contains("rgb") {
return line;
}
let hsv_rgb_regex = Regex::new(r"(hsv|rgb)\s*\{").expect("hsv_rgb_regex must be correct");
hsv_rgb_regex.replace(&line, "{").to_string()
}
fn tokenize_line(line: &str) -> impl Iterator<Item = Token> {
let comment_split = line.split_once(COMMENT_MARKER);
let (before_comment, mut comment) = if let Some((before_comment, comment)) = comment_split {
(before_comment, (!comment.is_empty()).then_some(comment))
} else {
(line, None)
};
before_comment.split_whitespace()
.filter_map(Token::from_noncomment)
.chain( std::iter::from_fn(move || {
if let Some(comment) = comment.take() {
Some(Token::from_comment(comment))
} else {
None
}
})
)
}
fn recognize_noncomment_token_kind(content: &str) -> TokenKind {
assert!(!content.is_empty());
match content {
"{" => TokenKind::OpeningBrace,
"}" => TokenKind::ClosingBrace,
_ => {
if ["=", "!=", "<", "<=", ">", ">="].contains(&content) {
TokenKind::KeyValueSeparator
} else {
assert!(!content.contains(COMMENT_MARKER));
debug_assert_eq!(content.split_whitespace().count(), 1);
TokenKind::CodeItem
}
}
}
}
mod error {
use std::{
fmt::Display,
num::ParseIntError
};
use thiserror::Error;
use crate::model::game::MapDataError;
use super::*;
#[derive(Error, Debug)]
pub enum LandedTitlesReadingError {
#[error("{0}")]
IoError(#[from] IoError),
#[error("{0}")]
MapDataError(#[from] MapDataError),
#[error("{0}")]
ParsingError(#[from] LandedTitlesParsingError)
}
#[derive(Error, Debug)]
#[error("error in line {line_number} at \"{token_content}\" : {kind}")]
pub struct LandedTitlesParsingError {
line_number: usize,
token_content: String,
kind: LandedTitlesParsingErrorKind
}
impl LandedTitlesParsingError {
pub fn from_unexpected_token(token_content: &str, line_number: usize) -> Self {
Self{
line_number,
token_content: String::from(token_content),
kind: LandedTitlesParsingErrorKind::UnexpectedToken
}
}
pub fn from_malformed_province_id(token_content: &str, line_number: usize, err: ParseIntError) -> Self {
Self{
line_number,
token_content: String::from(token_content),
kind: LandedTitlesParsingErrorKind::MalformedProvinceId(err)
}
}
}
#[derive(Debug)]
pub enum LandedTitlesParsingErrorKind {
UnexpectedToken,
#[allow(dead_code)]
MalformedProvinceId(ParseIntError)
}
impl Display for LandedTitlesParsingErrorKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(match self {
LandedTitlesParsingErrorKind::UnexpectedToken => format_args!("unexpected token"),
LandedTitlesParsingErrorKind::MalformedProvinceId(_) => format_args!("malformed province ID"),
})
}
}
}