// 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 nom::{ branch::alt, bytes::complete::{escaped_transform, is_not, tag}, character::complete::{alpha1, digit1, multispace0, multispace1}, combinator::{opt, value}, multi::{many0, many1, separated_list1}, sequence::delimited, Finish, IResult, }; use thiserror::Error; pub enum Warning {} #[derive(Debug, PartialEq, Error)] pub enum ParseError { #[error("An unknown error was found")] NomError(nom::error::Error), } impl From> for ParseError { fn from(err: nom::error::Error<&str>) -> Self { Self::NomError(nom::error::Error { input: err.input.to_owned(), code: err.code.clone(), }) } } /* impl From<(&str, VerboseErrorKind)> for impl From> for ParseError { fn from(err: nom::error::VerboseError<&str>) -> Self { Self::NomErrors( err.errors .into_iter() .map(|err| ParseError::from(err)) .collect(), ) /* Self::NomError(nom::error::Error { input: err.input.to_owned(), code: err.code.clone(), }) */ } } */ // 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) = opt(escaped_transform( is_not(r"\]"), '\\', alt((value("]", tag("\\]")), value("", tag("\\\n")))), ))(input)?; println!("--- {}", input); let (input, _) = tag("]")(input)?; Ok((input, value.map(|v| v.to_owned()).unwrap_or(String::new()))) } 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::>( "[There are hard linebreaks & soft linebreaks. Soft linebreaks...]", ) .unwrap(); assert_eq!( propval, "There are hard linebreaks & soft linebreaks. Soft linebreaks..." .to_owned() ); } #[test] fn it_parses_propvals_with_soft_linebreaks() { let (_, propval) = parse_propval::>( r"[Soft linebreaks are linebreaks preceeded by '\\' like this one >o\ k<. Hard line breaks are all other linebreaks.]", ) .unwrap(); assert_eq!( propval, r"Soft linebreaks are linebreaks preceeded by '\\' like this one >ok<. Hard line breaks are all other linebreaks." .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| {}); } */ }