From 9137909a64441c2df719ac498c7ad57bb418d5eb Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Mon, 3 Jul 2023 10:26:20 -0400 Subject: [PATCH] Start interpreting a tree as a game of Go --- go-sgf/src/go.rs | 269 +++++++++++++++++ go-sgf/src/lib.rs | 713 +-------------------------------------------- go-sgf/src/tree.rs | 480 ++++++++++++++++++++++++++++++ 3 files changed, 751 insertions(+), 711 deletions(-) create mode 100644 go-sgf/src/go.rs create mode 100644 go-sgf/src/tree.rs diff --git a/go-sgf/src/go.rs b/go-sgf/src/go.rs new file mode 100644 index 0000000..ee7b810 --- /dev/null +++ b/go-sgf/src/go.rs @@ -0,0 +1,269 @@ +// https://red-bean.com/sgf/user_guide/index.html +// https://red-bean.com/sgf/sgf4.html + +// todo: support collections in a file +// Properties to support. Remove each one as it gets support. +// B +// KO +// MN +// W +// AB +// AE +// AW +// PL +// C +// DM +// GB +// GW +// HO +// N +// UC +// V +// BM +// DO +// IT +// TE +// AR +// CR +// DD +// LB +// LN +// MA +// SL +// SQ +// TR +// AP +// CA +// FF +// GM +// ST +// SZ +// AN +// BR +// BT +// CP +// DT +// EV +// GN +// GC +// ON +// OT +// PB +// PC +// PW +// RE +// RO +// RU +// SO +// TM +// US +// WR +// WT +// BL +// OB +// OW +// WL +// FG +// PM +// VW + +use crate::tree::{parse_collection, parse_size, ParseSizeError, Size}; +use nom::IResult; + +#[derive(Debug)] +pub enum Error<'a> { + InvalidField, + InvalidBoardSize, + Incomplete, + InvalidSgf(nom::error::VerboseError<&'a str>), +} + +impl<'a> From>> for Error<'a> { + fn from(err: nom::Err>) -> Self { + match err { + nom::Err::Incomplete(_) => Error::Incomplete, + nom::Err::Error(e) => Error::InvalidSgf(e.clone()), + nom::Err::Failure(e) => Error::InvalidSgf(e.clone()), + } + } +} + +impl<'a> From for Error<'a> { + fn from(_: ParseSizeError) -> Self { + Self::InvalidBoardSize + } +} + +#[derive(Debug)] +pub struct GameTree { + pub file_format: i8, + pub app: Option, + pub game_type: GameType, + pub board_size: Size, + + pub text: String, +} + +pub struct GameInfo { + pub annotator: Option, + pub copyright: Option, + pub event: Option, + // Games can be played across multiple days, even multiple years. The format specifies + // shortcuts. + pub date_time: Vec, + pub location: Option, + // special rules for the round-number and type + pub round: Option, + pub ruleset: Option, + pub source: Option, + pub time_limits: Option, + pub game_keeper: Option, + + pub game_name: Option, + pub game_comments: Option, + + pub black_player: Option, + pub black_rank: Option, + pub black_team: Option, + + pub white_player: Option, + pub white_rank: Option, + pub white_team: Option, + + pub opening: Option, + pub overtime: Option, + pub result: Option, +} + +pub enum GameResult { + Annulled, + Draw, + Black(Win), + White(Win), +} + +pub enum Win { + Score(i32), + Resignation, + Forfeit, + Time, +} + +#[derive(Debug, PartialEq)] +pub enum GameType { + Go, + Unsupported, +} + +enum PropType { + Move, + Setup, + Root, + GameInfo, +} + +enum PropValue { + Empty, + Number, + Real, + Double, + Color, + SimpleText, + Text, + Point, + Move, + Stone, +} + +pub fn parse_sgf<'a>(input: &'a str) -> Result, Error<'a>> { + let (input, trees) = parse_collection::>(input)?; + + let games = trees + .into_iter() + .map(|tree| { + let file_format = match tree.sequence[0].find_prop("FF") { + Some(prop) => prop.values[0].parse::().unwrap(), + None => 4, + }; + let app = tree.sequence[0] + .find_prop("AP") + .map(|prop| prop.values[0].clone()); + let board_size = match tree.sequence[0].find_prop("SZ") { + Some(prop) => Size::try_from(prop.values[0].as_str())?, + None => Size { + width: 19, + height: 19, + }, + }; + + Ok(GameTree { + file_format, + + app, + game_type: GameType::Go, + board_size, + text: input.to_owned(), + }) + }) + .collect::, Error>>()?; + Ok(games) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tree::Size; + use std::fs::File; + use std::io::Read; + + const EXAMPLE: &'static str = "(;FF[4]C[root](;C[a];C[b](;C[c]) +(;C[d];C[e])) +(;C[f](;C[g];C[h];C[i]) +(;C[j])))"; + + fn with_text(text: &str, f: impl FnOnce(Vec)) { + let games = parse_sgf(text).unwrap(); + f(games); + } + + fn with_file(path: &std::path::Path, f: impl FnOnce(Vec)) { + let mut file = File::open(path).unwrap(); + let mut text = String::new(); + let _ = file.read_to_string(&mut text); + with_text(&text, f); + } + + #[test] + fn it_parses_game_root() { + with_text(EXAMPLE, |trees| { + assert_eq!(trees.len(), 1); + let tree = &trees[0]; + assert_eq!(tree.file_format, 4); + assert_eq!(tree.app, None); + assert_eq!(tree.game_type, GameType::Go); + assert_eq!( + tree.board_size, + Size { + width: 19, + height: 19 + } + ); + // assert_eq!(tree.text, EXAMPLE.to_owned()); + }); + + with_file(std::path::Path::new("test_data/print1.sgf"), |trees| { + assert_eq!(trees.len(), 1); + let tree = &trees[0]; + assert_eq!(tree.file_format, 4); + assert_eq!(tree.app, None); + assert_eq!(tree.game_type, GameType::Go); + assert_eq!( + tree.board_size, + Size { + width: 19, + height: 19 + } + ); + }); + } +} diff --git a/go-sgf/src/lib.rs b/go-sgf/src/lib.rs index bde52ac..fd695ee 100644 --- a/go-sgf/src/lib.rs +++ b/go-sgf/src/lib.rs @@ -1,81 +1,6 @@ -// https://red-bean.com/sgf/user_guide/index.html -// https://red-bean.com/sgf/sgf4.html +pub mod go; +pub mod tree; -// todo: support collections in a file -// Properties to support. Remove each one as it gets support. -// B -// KO -// MN -// W -// AB -// AE -// AW -// PL -// C -// DM -// GB -// GW -// HO -// N -// UC -// V -// BM -// DO -// IT -// TE -// AR -// CR -// DD -// LB -// LN -// MA -// SL -// SQ -// TR -// AP -// CA -// FF -// GM -// ST -// SZ -// AN -// BR -// BT -// CP -// DT -// EV -// GN -// GC -// ON -// OT -// PB -// PC -// PW -// RE -// RO -// RU -// SO -// TM -// US -// WR -// WT -// BL -// OB -// OW -// WL -// FG -// PM -// VW - -use nom::{ - branch::alt, - bytes::complete::{escaped, escaped_transform, is_not, tag}, - character::complete::{alpha1, digit1, multispace0, multispace1, none_of, one_of}, - combinator::{opt, value}, - multi::{many0, many1, separated_list1}, - sequence::delimited, - Finish, IResult, -}; use thiserror::Error; pub enum Warning {} @@ -115,637 +40,3 @@ impl From> for ParseError { } } */ - -// todo: support ST root node -#[derive(Debug)] -pub struct GameTree { - pub file_format: i8, - pub app: Option, - pub game_type: GameType, - pub board_size: Size, - - pub text: String, -} - -pub struct GameInfo { - pub annotator: Option, - pub copyright: Option, - pub event: Option, - // Games can be played across multiple days, even multiple years. The format specifies - // shortcuts. - pub date_time: Vec, - pub location: Option, - // special rules for the round-number and type - pub round: Option, - pub ruleset: Option, - pub source: Option, - pub time_limits: Option, - pub game_keeper: Option, - - pub game_name: Option, - pub game_comments: Option, - - pub black_player: Option, - pub black_rank: Option, - pub black_team: Option, - - pub white_player: Option, - pub white_rank: Option, - pub white_team: Option, - - pub opening: Option, - pub overtime: Option, - pub result: Option, -} - -pub enum GameResult { - Annulled, - Draw, - Black(Win), - White(Win), -} - -pub enum Win { - Score(i32), - Resignation, - Forfeit, - Time, -} - -#[derive(Debug, PartialEq)] -pub struct Size { - width: i32, - height: i32, -} - -#[derive(Debug, PartialEq)] -pub enum GameType { - Go, - Unsupported, -} - -// struct Sequence(Node); - -/* -struct Node { - // properties -} -*/ - -enum PropType { - Move, - Setup, - Root, - GameInfo, -} - -enum PropValue { - Empty, - Number, - Real, - Double, - Color, - SimpleText, - Text, - Point, - Move, - Stone, -} - -pub fn parse_sgf(input: &str) -> Result, ParseError> { - let (_, trees) = parse_collection::>(input).finish()?; - - trees - .into_iter() - .map(|tree| { - let file_format = match tree.sequence[0].find_prop("FF") { - Some(prop) => prop.values[0].parse::().unwrap(), - None => 4, - }; - let app = tree.sequence[0] - .find_prop("AP") - .map(|prop| prop.values[0].clone()); - let board_size = match tree.sequence[0].find_prop("SZ") { - Some(prop) => { - let (_, size) = - parse_size::>(prop.values[0].as_str()).finish()?; - size - } - None => Size { - width: 19, - height: 19, - }, - }; - - Ok(GameTree { - file_format, - - app, - game_type: GameType::Go, - board_size, - text: input.to_owned(), - }) - }) - .collect() -} - -#[derive(Debug, PartialEq)] -struct Tree { - sequence: Vec, - sub_sequences: Vec, -} - -impl ToString for Tree { - fn to_string(&self) -> String { - let sequence = self - .sequence - .iter() - .map(|node| node.to_string()) - .collect::(); - let subsequences = self - .sub_sequences - .iter() - .map(|seq| seq.to_string()) - .collect::(); - format!("({}{})", sequence, subsequences) - } -} - -#[derive(Debug, PartialEq)] -struct Node { - properties: Vec, -} - -impl ToString for Node { - fn to_string(&self) -> String { - let props = self - .properties - .iter() - .map(|prop| prop.to_string()) - .collect::(); - format!(";{}", props) - } -} - -impl Node { - fn find_prop(&self, ident: &str) -> Option { - self.properties - .iter() - .find(|prop| prop.ident == ident) - .cloned() - } -} - -#[derive(Clone, Debug, PartialEq)] -struct Property { - ident: String, - values: Vec, -} - -impl ToString for Property { - fn to_string(&self) -> String { - let values = self - .values - .iter() - .map(|val| format!("[{}]", val)) - .collect::(); - format!("{}{}", self.ident, values) - } -} - -fn parse_collection<'a, E: nom::error::ParseError<&'a str>>( - input: &'a str, -) -> IResult<&'a str, Vec, E> { - separated_list1(multispace1, parse_tree)(input) -} - -// note: must preserve unknown properties -// note: must fix or preserve illegally formatted game-info properties -// note: must correct or delete illegally foramtted properties, but display a warning -fn parse_tree<'a, E: nom::error::ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, Tree, E> { - println!("::: parse_tree: {}", input); - let (input, _) = multispace0(input)?; - delimited(tag("("), parse_sequence, tag(")"))(input) -} - -fn parse_sequence<'a, E: nom::error::ParseError<&'a str>>( - input: &'a str, -) -> IResult<&'a str, Tree, E> { - println!("::: parse_sequence: {}", input); - let (input, _) = multispace0(input)?; - let (input, nodes) = many1(parse_node)(input)?; - let (input, sub_sequences) = many0(parse_tree)(input)?; - - Ok(( - input, - Tree { - sequence: nodes, - sub_sequences, - }, - )) -} - -fn parse_node<'a, E: nom::error::ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, Node, E> { - println!("::: parse_node: {}", input); - let (input, _) = multispace0(input)?; - let (input, _) = tag(";")(input)?; - let (input, properties) = many1(parse_property)(input)?; - Ok((input, Node { properties })) -} - -fn parse_property<'a, E: nom::error::ParseError<&'a str>>( - input: &'a str, -) -> IResult<&'a str, Property, E> { - println!(":: parse_property: {}", input); - let (input, _) = multispace0(input)?; - let (input, ident) = alpha1(input)?; - let (input, values) = many1(parse_propval)(input)?; - - let values = values - .into_iter() - .map(|v| v.to_owned()) - .collect::>(); - Ok(( - input, - Property { - ident: ident.to_owned(), - values, - }, - )) -} - -fn parse_propval<'a, E: nom::error::ParseError<&'a str>>( - input: &'a str, -) -> IResult<&'a str, String, E> { - let (input, _) = multispace0(input)?; - println!("- {}", input); - - let (input, _) = tag("[")(input)?; - println!("-- {}", input); - - let (input, value) = parse_propval_text(input)?; - println!("--- {}", input); - - let (input, _) = tag("]")(input)?; - - Ok((input, value.unwrap_or(String::new()))) -} - -fn parse_propval_text<'a, E: nom::error::ParseError<&'a str>>( - input: &'a str, -) -> IResult<&'a str, Option, E> { - let (input, value) = opt(escaped_transform( - none_of("\\]"), - '\\', - alt(( - value("]", tag("]")), - value("\\", tag("\\")), - value("", tag("\n")), - )), - ))(input)?; - Ok((input, value.map(|v| v.to_owned()))) -} - -fn parse_size<'a, E: nom::error::ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, Size, E> { - let (input, dimensions) = separated_list1(tag(":"), digit1)(input)?; - let (width, height) = match dimensions.as_slice() { - [width] => (width.parse::().unwrap(), width.parse::().unwrap()), - [width, height] => ( - width.parse::().unwrap(), - height.parse::().unwrap(), - ), - _ => (19, 19), - }; - Ok((input, Size { width, height })) -} - -#[cfg(test)] -mod tests { - use std::{fs::File, io::Read}; - - use super::*; - - const EXAMPLE: &'static str = "(;FF[4]C[root](;C[a];C[b](;C[c]) -(;C[d];C[e])) -(;C[f](;C[g];C[h];C[i]) -(;C[j])))"; - - #[test] - fn it_can_parse_properties() { - let (_, prop) = parse_property::>("C[a]").unwrap(); - assert_eq!( - prop, - Property { - ident: "C".to_owned(), - values: vec!["a".to_owned()] - } - ); - - let (_, prop) = parse_property::>("C[a][b][c]").unwrap(); - assert_eq!( - prop, - Property { - ident: "C".to_owned(), - values: vec!["a".to_owned(), "b".to_owned(), "c".to_owned()] - } - ); - } - - #[test] - fn it_can_parse_a_standalone_node() { - let (_, node) = parse_node::>(";B[ab]").unwrap(); - - assert_eq!( - node, - Node { - properties: vec![Property { - ident: "B".to_owned(), - values: vec!["ab".to_owned()] - }] - } - ); - - let (_, node) = - parse_node::>(";B[ab];W[dp];B[pq]C[some comments]") - .unwrap(); - - assert_eq!( - node, - Node { - properties: vec![Property { - ident: "B".to_owned(), - values: vec!["ab".to_owned()] - }] - } - ); - } - - #[test] - fn it_can_parse_a_simple_sequence() { - let (_, sequence) = - parse_tree::>("(;B[ab];W[dp];B[pq]C[some comments])") - .unwrap(); - - assert_eq!( - sequence, - Tree { - sequence: vec![ - Node { - properties: vec![Property { - ident: "B".to_owned(), - values: vec!["ab".to_owned()] - }] - }, - Node { - properties: vec![Property { - ident: "W".to_owned(), - values: vec!["dp".to_owned()] - }] - }, - Node { - properties: vec![ - Property { - ident: "B".to_owned(), - values: vec!["pq".to_owned()] - }, - Property { - ident: "C".to_owned(), - values: vec!["some comments".to_owned()] - } - ] - } - ], - sub_sequences: vec![], - } - ); - } - - #[test] - fn it_can_parse_a_sequence_with_subsequences() { - let text = "(;C[a];C[b](;C[c])(;C[d];C[e]))"; - let (_, sequence) = parse_tree::>(text).unwrap(); - - let main_sequence = vec![ - Node { - properties: vec![Property { - ident: "C".to_owned(), - values: vec!["a".to_owned()], - }], - }, - Node { - properties: vec![Property { - ident: "C".to_owned(), - values: vec!["b".to_owned()], - }], - }, - ]; - let subsequence_1 = Tree { - sequence: vec![Node { - properties: vec![Property { - ident: "C".to_owned(), - values: vec!["c".to_owned()], - }], - }], - sub_sequences: vec![], - }; - let subsequence_2 = Tree { - sequence: vec![ - Node { - properties: vec![Property { - ident: "C".to_owned(), - values: vec!["d".to_owned()], - }], - }, - Node { - properties: vec![Property { - ident: "C".to_owned(), - values: vec!["e".to_owned()], - }], - }, - ], - sub_sequences: vec![], - }; - - assert_eq!( - sequence, - Tree { - sequence: main_sequence, - sub_sequences: vec![subsequence_1, subsequence_2], - } - ); - } - - #[test] - fn it_can_parse_example_1() { - let (_, ex_tree) = parse_tree::>(EXAMPLE).unwrap(); - assert_eq!(ex_tree.sequence.len(), 1); - - assert_eq!(ex_tree.sequence[0].properties.len(), 2); - assert_eq!( - ex_tree.sequence[0].properties[0], - Property { - ident: "FF".to_owned(), - values: vec!["4".to_owned()] - } - ); - assert_eq!(ex_tree.sub_sequences.len(), 2); - - assert_eq!(ex_tree.sub_sequences[0].sequence.len(), 2); - assert_eq!( - ex_tree.sub_sequences[0].sequence, - vec![ - Node { - properties: vec![Property { - ident: "C".to_owned(), - values: vec!["a".to_owned()] - }] - }, - Node { - properties: vec![Property { - ident: "C".to_owned(), - values: vec!["b".to_owned()] - }] - }, - ] - ); - assert_eq!(ex_tree.sub_sequences[0].sub_sequences.len(), 2); - } - - #[test] - fn it_can_regenerate_the_tree() { - let (_, tree1) = parse_tree::>(EXAMPLE).unwrap(); - assert_eq!( - tree1.to_string(), - "(;FF[4]C[root](;C[a];C[b](;C[c])(;C[d];C[e]))(;C[f](;C[g];C[h];C[i])(;C[j])))" - ); - let (_, tree2) = parse_tree::>(&tree1.to_string()).unwrap(); - assert_eq!(tree1, tree2); - } - - #[test] - fn it_parses_propvals() { - let (_, propval) = parse_propval::>("[]").unwrap(); - assert_eq!(propval, "".to_owned()); - - let (_, propval) = - parse_propval::>("[normal propval]").unwrap(); - assert_eq!(propval, "normal propval".to_owned()); - - let (_, propval) = - parse_propval::>(r"[need an [escape\] in the propval]") - .unwrap(); - assert_eq!(propval, "need an [escape] in the propval".to_owned()); - } - - #[test] - fn it_parses_propvals_with_hard_linebreaks() { - let (_, propval) = parse_propval_text::>( - "There are hard linebreaks & soft linebreaks. -Soft linebreaks...", - ) - .unwrap(); - assert_eq!( - propval, - Some( - "There are hard linebreaks & soft linebreaks. -Soft linebreaks..." - .to_owned() - ) - ); - } - - #[test] - fn it_parses_propvals_with_escaped_closing_brackets() { - let (_, propval) = - parse_propval_text::>(r"escaped closing \] bracket") - .unwrap(); - assert_eq!( - propval, - Some(r"escaped closing ] bracket".to_owned()).to_owned() - ); - } - - #[test] - fn it_parses_propvals_with_soft_linebreaks() { - let (_, propval) = parse_propval_text::>( - r"Soft linebreaks are linebreaks preceeded by '\\' like this one >o\ -k<. Hard line breaks are all other linebreaks.", - ) - .unwrap(); - assert_eq!( - propval, - Some("Soft linebreaks are linebreaks preceeded by '\\' like this one >ok<. Hard line breaks are all other linebreaks.".to_owned()) - .to_owned() - ); - } - - fn with_text(text: &str, f: impl FnOnce(Vec)) { - f(parse_sgf(text).unwrap()); - } - - fn with_file(path: &std::path::Path, f: impl FnOnce(Vec)) { - let mut file = File::open(path).unwrap(); - let mut text = String::new(); - let _ = file.read_to_string(&mut text); - with_text(&text, f); - } - - #[test] - fn it_parses_game_root() { - with_text(EXAMPLE, |trees| { - assert_eq!(trees.len(), 1); - let tree = &trees[0]; - assert_eq!(tree.file_format, 4); - assert_eq!(tree.app, None); - assert_eq!(tree.game_type, GameType::Go); - assert_eq!( - tree.board_size, - Size { - width: 19, - height: 19 - } - ); - assert_eq!(tree.text, EXAMPLE.to_owned()); - }); - - with_file(std::path::Path::new("test_data/print1.sgf"), |trees| { - assert_eq!(trees.len(), 1); - let tree = &trees[0]; - assert_eq!(tree.file_format, 4); - assert_eq!(tree.app, None); - assert_eq!(tree.game_type, GameType::Go); - assert_eq!( - tree.board_size, - Size { - width: 19, - height: 19 - } - ); - }); - } - - /* - #[test] - fn it_parses_linebreaks() { - with_file( - std::path::Path::new("test_data/linebreak_tests.sgf"), - |tree| {}, - ); - } - - #[test] - fn it_parses_ff4_a() { - with_file(std::path::Path::new("test_data/ff4_a.sgf"), |tree| {}); - } - - #[test] - fn it_parses_ff4_b() { - with_file(std::path::Path::new("test_data/ff4_b.sgf"), |tree| {}); - } - - #[test] - fn it_parses_ff4_ex() { - with_file(std::path::Path::new("test_data/ff4_ex.sgf"), |tree| {}); - } - */ -} diff --git a/go-sgf/src/tree.rs b/go-sgf/src/tree.rs new file mode 100644 index 0000000..2840eef --- /dev/null +++ b/go-sgf/src/tree.rs @@ -0,0 +1,480 @@ +use std::num::ParseIntError; + +use nom::{ + branch::alt, + bytes::complete::{escaped_transform, tag}, + character::complete::{alpha1, digit1, multispace0, multispace1, none_of}, + combinator::{opt, value}, + multi::{many0, many1, separated_list1}, + sequence::delimited, + IResult, +}; + +#[derive(Debug)] +pub enum ParseSizeError { + ParseIntError(ParseIntError), + InsufficientArguments, +} + +impl From for ParseSizeError { + fn from(e: ParseIntError) -> Self { + Self::ParseIntError(e) + } +} + +#[derive(Debug, PartialEq)] +pub struct Size { + pub width: i32, + pub height: i32, +} + +impl TryFrom<&str> for Size { + type Error = ParseSizeError; + fn try_from(s: &str) -> Result { + let parts = s + .split(':') + .map(|v| v.parse::()) + .collect::, ParseIntError>>()?; + match parts[..] { + [width, height, ..] => Ok(Size { width, height }), + [dim] => Ok(Size { + width: dim, + height: dim, + }), + [] => Err(ParseSizeError::InsufficientArguments), + } + } +} + +#[derive(Debug, PartialEq)] +pub struct Tree { + pub sequence: Vec, + pub sub_sequences: Vec, +} + +impl ToString for Tree { + fn to_string(&self) -> String { + let sequence = self + .sequence + .iter() + .map(|node| node.to_string()) + .collect::(); + let subsequences = self + .sub_sequences + .iter() + .map(|seq| seq.to_string()) + .collect::(); + format!("({}{})", sequence, subsequences) + } +} + +#[derive(Debug, PartialEq)] +pub struct Node { + pub properties: Vec, +} + +impl ToString for Node { + fn to_string(&self) -> String { + let props = self + .properties + .iter() + .map(|prop| prop.to_string()) + .collect::(); + format!(";{}", props) + } +} + +impl Node { + pub fn find_prop(&self, ident: &str) -> Option { + self.properties + .iter() + .find(|prop| prop.ident == ident) + .cloned() + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct Property { + pub ident: String, + pub values: Vec, +} + +impl ToString for Property { + fn to_string(&self) -> String { + let values = self + .values + .iter() + .map(|val| format!("[{}]", val)) + .collect::(); + format!("{}{}", self.ident, values) + } +} + +pub fn parse_collection<'a, E: nom::error::ParseError<&'a str>>( + input: &'a str, +) -> IResult<&'a str, Vec, E> { + separated_list1(multispace1, parse_tree)(input) +} + +// note: must preserve unknown properties +// note: must fix or preserve illegally formatted game-info properties +// note: must correct or delete illegally foramtted properties, but display a warning +fn parse_tree<'a, E: nom::error::ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, Tree, E> { + println!("::: parse_tree: {}", input); + let (input, _) = multispace0(input)?; + delimited(tag("("), parse_sequence, tag(")"))(input) +} + +fn parse_sequence<'a, E: nom::error::ParseError<&'a str>>( + input: &'a str, +) -> IResult<&'a str, Tree, E> { + println!("::: parse_sequence: {}", input); + let (input, _) = multispace0(input)?; + let (input, nodes) = many1(parse_node)(input)?; + let (input, sub_sequences) = many0(parse_tree)(input)?; + + Ok(( + input, + Tree { + sequence: nodes, + sub_sequences, + }, + )) +} + +fn parse_node<'a, E: nom::error::ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, Node, E> { + println!("::: parse_node: {}", input); + let (input, _) = multispace0(input)?; + let (input, _) = tag(";")(input)?; + let (input, properties) = many1(parse_property)(input)?; + Ok((input, Node { properties })) +} + +fn parse_property<'a, E: nom::error::ParseError<&'a str>>( + input: &'a str, +) -> IResult<&'a str, Property, E> { + println!(":: parse_property: {}", input); + let (input, _) = multispace0(input)?; + let (input, ident) = alpha1(input)?; + let (input, values) = many1(parse_propval)(input)?; + + let values = values + .into_iter() + .map(|v| v.to_owned()) + .collect::>(); + Ok(( + input, + Property { + ident: ident.to_owned(), + values, + }, + )) +} + +fn parse_propval<'a, E: nom::error::ParseError<&'a str>>( + input: &'a str, +) -> IResult<&'a str, String, E> { + let (input, _) = multispace0(input)?; + println!("- {}", input); + + let (input, _) = tag("[")(input)?; + println!("-- {}", input); + + let (input, value) = parse_propval_text(input)?; + println!("--- {}", input); + + let (input, _) = tag("]")(input)?; + + Ok((input, value.unwrap_or(String::new()))) +} + +fn parse_propval_text<'a, E: nom::error::ParseError<&'a str>>( + input: &'a str, +) -> IResult<&'a str, Option, E> { + let (input, value) = opt(escaped_transform( + none_of("\\]"), + '\\', + alt(( + value("]", tag("]")), + value("\\", tag("\\")), + value("", tag("\n")), + )), + ))(input)?; + Ok((input, value.map(|v| v.to_owned()))) +} + +pub fn parse_size<'a, E: nom::error::ParseError<&'a str>>( + input: &'a str, +) -> IResult<&'a str, Size, E> { + let (input, dimensions) = separated_list1(tag(":"), digit1)(input)?; + let (width, height) = match dimensions.as_slice() { + [width] => (width.parse::().unwrap(), width.parse::().unwrap()), + [width, height] => ( + width.parse::().unwrap(), + height.parse::().unwrap(), + ), + _ => (19, 19), + }; + Ok((input, Size { width, height })) +} + +#[cfg(test)] +mod test { + use std::{fs::File, io::Read}; + + use super::*; + + const EXAMPLE: &'static str = "(;FF[4]C[root](;C[a];C[b](;C[c]) +(;C[d];C[e])) +(;C[f](;C[g];C[h];C[i]) +(;C[j])))"; + + #[test] + fn it_can_parse_properties() { + let (_, prop) = parse_property::>("C[a]").unwrap(); + assert_eq!( + prop, + Property { + ident: "C".to_owned(), + values: vec!["a".to_owned()] + } + ); + + let (_, prop) = parse_property::>("C[a][b][c]").unwrap(); + assert_eq!( + prop, + Property { + ident: "C".to_owned(), + values: vec!["a".to_owned(), "b".to_owned(), "c".to_owned()] + } + ); + } + + #[test] + fn it_can_parse_a_standalone_node() { + let (_, node) = parse_node::>(";B[ab]").unwrap(); + + assert_eq!( + node, + Node { + properties: vec![Property { + ident: "B".to_owned(), + values: vec!["ab".to_owned()] + }] + } + ); + + let (_, node) = + parse_node::>(";B[ab];W[dp];B[pq]C[some comments]") + .unwrap(); + + assert_eq!( + node, + Node { + properties: vec![Property { + ident: "B".to_owned(), + values: vec!["ab".to_owned()] + }] + } + ); + } + + #[test] + fn it_can_parse_a_simple_sequence() { + let (_, sequence) = + parse_tree::>("(;B[ab];W[dp];B[pq]C[some comments])") + .unwrap(); + + assert_eq!( + sequence, + Tree { + sequence: vec![ + Node { + properties: vec![Property { + ident: "B".to_owned(), + values: vec!["ab".to_owned()] + }] + }, + Node { + properties: vec![Property { + ident: "W".to_owned(), + values: vec!["dp".to_owned()] + }] + }, + Node { + properties: vec![ + Property { + ident: "B".to_owned(), + values: vec!["pq".to_owned()] + }, + Property { + ident: "C".to_owned(), + values: vec!["some comments".to_owned()] + } + ] + } + ], + sub_sequences: vec![], + } + ); + } + + #[test] + fn it_can_parse_a_sequence_with_subsequences() { + let text = "(;C[a];C[b](;C[c])(;C[d];C[e]))"; + let (_, sequence) = parse_tree::>(text).unwrap(); + + let main_sequence = vec![ + Node { + properties: vec![Property { + ident: "C".to_owned(), + values: vec!["a".to_owned()], + }], + }, + Node { + properties: vec![Property { + ident: "C".to_owned(), + values: vec!["b".to_owned()], + }], + }, + ]; + let subsequence_1 = Tree { + sequence: vec![Node { + properties: vec![Property { + ident: "C".to_owned(), + values: vec!["c".to_owned()], + }], + }], + sub_sequences: vec![], + }; + let subsequence_2 = Tree { + sequence: vec![ + Node { + properties: vec![Property { + ident: "C".to_owned(), + values: vec!["d".to_owned()], + }], + }, + Node { + properties: vec![Property { + ident: "C".to_owned(), + values: vec!["e".to_owned()], + }], + }, + ], + sub_sequences: vec![], + }; + + assert_eq!( + sequence, + Tree { + sequence: main_sequence, + sub_sequences: vec![subsequence_1, subsequence_2], + } + ); + } + + #[test] + fn it_can_parse_example_1() { + let (_, ex_tree) = parse_tree::>(EXAMPLE).unwrap(); + assert_eq!(ex_tree.sequence.len(), 1); + + assert_eq!(ex_tree.sequence[0].properties.len(), 2); + assert_eq!( + ex_tree.sequence[0].properties[0], + Property { + ident: "FF".to_owned(), + values: vec!["4".to_owned()] + } + ); + assert_eq!(ex_tree.sub_sequences.len(), 2); + + assert_eq!(ex_tree.sub_sequences[0].sequence.len(), 2); + assert_eq!( + ex_tree.sub_sequences[0].sequence, + vec![ + Node { + properties: vec![Property { + ident: "C".to_owned(), + values: vec!["a".to_owned()] + }] + }, + Node { + properties: vec![Property { + ident: "C".to_owned(), + values: vec!["b".to_owned()] + }] + }, + ] + ); + assert_eq!(ex_tree.sub_sequences[0].sub_sequences.len(), 2); + } + + #[test] + fn it_can_regenerate_the_tree() { + let (_, tree1) = parse_tree::>(EXAMPLE).unwrap(); + assert_eq!( + tree1.to_string(), + "(;FF[4]C[root](;C[a];C[b](;C[c])(;C[d];C[e]))(;C[f](;C[g];C[h];C[i])(;C[j])))" + ); + let (_, tree2) = parse_tree::>(&tree1.to_string()).unwrap(); + assert_eq!(tree1, tree2); + } + + #[test] + fn it_parses_propvals() { + let (_, propval) = parse_propval::>("[]").unwrap(); + assert_eq!(propval, "".to_owned()); + + let (_, propval) = + parse_propval::>("[normal propval]").unwrap(); + assert_eq!(propval, "normal propval".to_owned()); + + let (_, propval) = + parse_propval::>(r"[need an [escape\] in the propval]") + .unwrap(); + assert_eq!(propval, "need an [escape] in the propval".to_owned()); + } + + #[test] + fn it_parses_propvals_with_hard_linebreaks() { + let (_, propval) = parse_propval_text::>( + "There are hard linebreaks & soft linebreaks. +Soft linebreaks...", + ) + .unwrap(); + assert_eq!( + propval, + Some( + "There are hard linebreaks & soft linebreaks. +Soft linebreaks..." + .to_owned() + ) + ); + } + + #[test] + fn it_parses_propvals_with_escaped_closing_brackets() { + let (_, propval) = + parse_propval_text::>(r"escaped closing \] bracket") + .unwrap(); + assert_eq!( + propval, + Some(r"escaped closing ] bracket".to_owned()).to_owned() + ); + } + + #[test] + fn it_parses_propvals_with_soft_linebreaks() { + let (_, propval) = parse_propval_text::>( + r"Soft linebreaks are linebreaks preceeded by '\\' like this one >o\ +k<. Hard line breaks are all other linebreaks.", + ) + .unwrap(); + assert_eq!( + propval, + Some("Soft linebreaks are linebreaks preceeded by '\\' like this one >ok<. Hard line breaks are all other linebreaks.".to_owned()) + .to_owned() + ); + } +}