monorepo/sgf/src/parser.rs

1007 lines
30 KiB
Rust

use crate::{Color, Error, GameResult};
use nom::{
branch::alt,
bytes::complete::{escaped_transform, tag, take_until1},
character::complete::{alpha1, digit1, multispace0, multispace1, none_of},
combinator::{opt, value},
error::ParseError,
multi::{many0, many1, separated_list1},
IResult, Parser,
};
use std::{num::ParseIntError, time::Duration};
impl From<ParseSizeError> for Error {
fn from(_: ParseSizeError) -> Self {
Self::InvalidBoardSize
}
}
#[derive(Debug)]
pub enum ParseSizeError {
ParseIntError(ParseIntError),
InsufficientArguments,
}
impl From<ParseIntError> for ParseSizeError {
fn from(e: ParseIntError) -> Self {
Self::ParseIntError(e)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Double {
Normal,
Emphasized,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Annotation {
BadMove,
DoubtfulMove,
InterestingMove,
Tesuji,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Evaluation {
Even,
GoodForBlack,
GoodForWhite,
Unclear,
}
#[derive(Clone, Debug, PartialEq)]
pub enum GameType {
Go,
Othello,
Chess,
GomokuRenju,
NineMensMorris,
Backgammon,
ChineseChess,
Shogi,
LinesOfAction,
Ataxx,
Hex,
Jungle,
Neutron,
PhilosophersFootball,
Quadrature,
Trax,
Tantrix,
Amazons,
Octi,
Gess,
Twixt,
Zertz,
Plateau,
Yinsh,
Punct,
Gobblet,
Hive,
Exxit,
Hnefatal,
Kuba,
Tripples,
Chase,
TumblingDown,
Sahara,
Byte,
Focus,
Dvonn,
Tamsk,
Gipf,
Kropki,
Other(String),
}
impl From<&str> for GameType {
fn from(s: &str) -> Self {
match s {
"1" => Self::Go,
"2" => Self::Othello,
"3" => Self::Chess,
"4" => Self::GomokuRenju,
"5" => Self::NineMensMorris,
"6" => Self::Backgammon,
"7" => Self::ChineseChess,
"8" => Self::Shogi,
"9" => Self::LinesOfAction,
"10" => Self::Ataxx,
"11" => Self::Hex,
"12" => Self::Jungle,
"13" => Self::Neutron,
"14" => Self::PhilosophersFootball,
"15" => Self::Quadrature,
"16" => Self::Trax,
"17" => Self::Tantrix,
"18" => Self::Amazons,
"19" => Self::Octi,
"20" => Self::Gess,
"21" => Self::Twixt,
"22" => Self::Zertz,
"23" => Self::Plateau,
"24" => Self::Yinsh,
"25" => Self::Punct,
"26" => Self::Gobblet,
"27" => Self::Hive,
"28" => Self::Exxit,
"29" => Self::Hnefatal,
"30" => Self::Kuba,
"31" => Self::Tripples,
"32" => Self::Chase,
"33" => Self::TumblingDown,
"34" => Self::Sahara,
"35" => Self::Byte,
"36" => Self::Focus,
"37" => Self::Dvonn,
"38" => Self::Tamsk,
"39" => Self::Gipf,
"40" => Self::Kropki,
_ => Self::Other(s.to_owned()),
}
}
}
impl From<&GameType> for String {
fn from(g: &GameType) -> String {
match g {
GameType::Go => "1".to_owned(),
GameType::Othello => "2".to_owned(),
GameType::Chess => "3".to_owned(),
GameType::GomokuRenju => "4".to_owned(),
GameType::NineMensMorris => "5".to_owned(),
GameType::Backgammon => "6".to_owned(),
GameType::ChineseChess => "7".to_owned(),
GameType::Shogi => "8".to_owned(),
GameType::LinesOfAction => "9".to_owned(),
GameType::Ataxx => "10".to_owned(),
GameType::Hex => "11".to_owned(),
GameType::Jungle => "12".to_owned(),
GameType::Neutron => "13".to_owned(),
GameType::PhilosophersFootball => "14".to_owned(),
GameType::Quadrature => "15".to_owned(),
GameType::Trax => "16".to_owned(),
GameType::Tantrix => "17".to_owned(),
GameType::Amazons => "18".to_owned(),
GameType::Octi => "19".to_owned(),
GameType::Gess => "20".to_owned(),
GameType::Twixt => "21".to_owned(),
GameType::Zertz => "22".to_owned(),
GameType::Plateau => "23".to_owned(),
GameType::Yinsh => "24".to_owned(),
GameType::Punct => "25".to_owned(),
GameType::Gobblet => "26".to_owned(),
GameType::Hive => "27".to_owned(),
GameType::Exxit => "28".to_owned(),
GameType::Hnefatal => "29".to_owned(),
GameType::Kuba => "30".to_owned(),
GameType::Tripples => "31".to_owned(),
GameType::Chase => "32".to_owned(),
GameType::TumblingDown => "33".to_owned(),
GameType::Sahara => "34".to_owned(),
GameType::Byte => "35".to_owned(),
GameType::Focus => "36".to_owned(),
GameType::Dvonn => "37".to_owned(),
GameType::Tamsk => "38".to_owned(),
GameType::Gipf => "39".to_owned(),
GameType::Kropki => "40".to_owned(),
GameType::Other(v) => v.clone(),
}
}
}
impl ToString for GameType {
fn to_string(&self) -> String {
String::from(self)
}
}
#[derive(Clone, 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<Self, Self::Error> {
let parts = s
.split(':')
.map(|v| v.parse::<i32>())
.collect::<Result<Vec<i32>, ParseIntError>>()?;
match parts[..] {
[width, height, ..] => Ok(Size { width, height }),
[dim] => Ok(Size {
width: dim,
height: dim,
}),
[] => Err(ParseSizeError::InsufficientArguments),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Position(String);
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct PositionList(pub Vec<String>);
impl PositionList {
pub fn compressed_list(&self) -> String {
self.0
.iter()
.map(|v| v.clone())
.collect::<Vec<String>>()
.join(":")
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Tree {
pub root: Node,
}
impl ToString for Tree {
fn to_string(&self) -> String {
format!("({})", self.root.to_string())
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Node {
pub properties: Vec<Property>,
pub next: Vec<Node>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum SetupInstr {
Piece((Color, String)),
Clear(String),
}
impl Node {
pub fn mv(&self) -> Option<(Color, String)> {
self.find_by(|prop| match prop {
Property::Move(val) => Some(val.clone()),
_ => None,
})
}
pub fn setup(&self) -> Option<Vec<SetupInstr>> {
let mut setup = Vec::new();
for prop in self.properties.iter() {
match prop {
Property::SetupBlackStones(positions) => {
setup.append(
&mut positions
.0
.iter()
.map(|pos| SetupInstr::Piece((Color::Black, pos.clone())))
.collect::<Vec<SetupInstr>>(),
);
}
Property::SetupWhiteStones(positions) => {
setup.append(
&mut positions
.0
.iter()
.map(|pos| SetupInstr::Piece((Color::White, pos.clone())))
.collect::<Vec<SetupInstr>>(),
);
}
Property::ClearStones(positions) => {
setup.append(
&mut positions
.0
.iter()
.map(|pos| SetupInstr::Clear(pos.clone()))
.collect::<Vec<SetupInstr>>(),
);
}
_ => unimplemented!(),
}
}
if setup.len() > 0 {
Some(setup)
} else {
None
}
}
fn find_by<F, R>(&self, f: F) -> Option<R>
where
F: FnMut(&Property) -> Option<R>,
{
self.properties.iter().filter_map(f).next()
}
}
impl ToString for Node {
fn to_string(&self) -> String {
let props = self
.properties
.iter()
.map(|prop| prop.to_string())
.collect::<String>();
let next = if self.next.len() == 1 {
self.next
.iter()
.map(|node| node.to_string())
.collect::<Vec<String>>()
.join("")
} else {
self.next
.iter()
.map(|node| format!("({})", node.to_string()))
.collect::<Vec<String>>()
.join("")
};
format!(";{}{}", props, next)
}
}
// KO
// MN
// N
// AR
// CR
// DD
// LB
// LN
// MA
// SL
// SQ
// TR
// OB
// OW
// FG
// PM
// VW
#[derive(Clone, Debug, PartialEq)]
pub enum Property {
// B, W
Move((Color, String)),
// C
Comment(String),
// BM, DO, IT, TE
Annotation(Annotation),
// AP
Application(String),
// CA
Charset(String),
// FF
FileFormat(i32),
// GM
GameType(GameType),
// ST
VariationDisplay,
// SZ
BoardSize(Size),
// AB
SetupBlackStones(PositionList),
// AE
ClearStones(PositionList),
// AW
SetupWhiteStones(PositionList),
// PL
NextPlayer(Color),
// DM, GB, GW, UC
Evaluation(Evaluation),
// HO
Hotspot,
// V
Value(f32),
// AN
Annotator(String),
// BR
BlackRank(String),
// BT
BlackTeam(String),
// CP
Copyright(String),
// DT
EventDates(Vec<chrono::NaiveDate>),
// EV
EventName(String),
// GN
GameName(String),
// GC
ExtraGameInformation(String),
// ON
GameOpening(String),
// OT
Overtime(String),
// PB
BlackPlayer(String),
// PC
GameLocation(String),
// PW
WhitePlayer(String),
// RE
Result(GameResult),
// RO
Round(String),
// RU
Ruleset(String),
// SO
Source(String),
// TM
TimeLimit(Duration),
// US
User(String),
// WR
WhiteRank(String),
// WT
WhiteTeam(String),
// BL, WL
TimeLeft((Color, Duration)),
Unknown(UnknownProperty),
}
#[derive(Clone, Debug, PartialEq)]
pub struct UnknownProperty {
pub ident: String,
pub value: String,
}
impl ToString for Property {
fn to_string(&self) -> String {
match self {
Property::Move((color, position)) => {
format!("{}[{}]", color.abbreviation(), position)
}
Property::TimeLeft((color, time)) => {
format!("{}[{}]", color.abbreviation(), time.as_secs())
}
Property::Comment(value) => format!("C[{}]", value),
Property::Annotation(Annotation::BadMove) => "BM[]".to_owned(),
Property::Annotation(Annotation::DoubtfulMove) => "DO[]".to_owned(),
Property::Annotation(Annotation::InterestingMove) => "IT[]".to_owned(),
Property::Annotation(Annotation::Tesuji) => "TE[]".to_owned(),
Property::Application(app) => format!("AP[{}]", app),
Property::Charset(set) => format!("CA[{}]", set),
Property::FileFormat(ff) => format!("FF[{}]", ff),
Property::GameType(gt) => format!("GM[{}]", gt.to_string()),
Property::VariationDisplay => unimplemented!(),
Property::BoardSize(Size { width, height }) => {
if width == height {
format!("SZ[{}]", width)
} else {
format!("SZ[{}:{}]", width, height)
}
}
Property::SetupBlackStones(positions) => {
format!("AB[{}]", positions.compressed_list(),)
}
Property::ClearStones(positions) => {
format!("AE[{}]", positions.compressed_list(),)
}
Property::SetupWhiteStones(positions) => {
format!("AW[{}]", positions.compressed_list(),)
}
Property::NextPlayer(color) => format!("PL[{}]", color.abbreviation()),
Property::Evaluation(Evaluation::Even) => "DM[]".to_owned(),
Property::Evaluation(Evaluation::GoodForBlack) => "GB[]".to_owned(),
Property::Evaluation(Evaluation::GoodForWhite) => "GW[]".to_owned(),
Property::Evaluation(Evaluation::Unclear) => "UC[]".to_owned(),
Property::Hotspot => "HO[]".to_owned(),
Property::Value(value) => format!("V[{}]", value),
Property::Annotator(value) => format!("AN[{}]", value),
Property::BlackRank(value) => format!("BR[{}]", value),
Property::BlackTeam(value) => format!("BT[{}]", value),
Property::Copyright(value) => format!("CP[{}]", value),
Property::EventDates(_) => unimplemented!(),
Property::EventName(value) => format!("EV[{}]", value),
Property::GameName(value) => format!("GN[{}]", value),
Property::ExtraGameInformation(value) => format!("GC[{}]", value),
Property::GameOpening(value) => format!("ON[{}]", value),
Property::Overtime(value) => format!("OT[{}]", value),
Property::BlackPlayer(value) => format!("PB[{}]", value),
Property::GameLocation(value) => format!("PC[{}]", value),
Property::WhitePlayer(value) => format!("PW[{}]", value),
Property::Result(_) => unimplemented!(),
Property::Round(value) => format!("RO[{}]", value),
Property::Ruleset(value) => format!("RU[{}]", value),
Property::Source(value) => format!("SO[{}]", value),
Property::TimeLimit(value) => format!("TM[{}]", value.as_secs()),
Property::User(value) => format!("US[{}]", value),
Property::WhiteRank(value) => format!("WR[{}]", value),
Property::WhiteTeam(value) => format!("WT[{}]", value),
Property::Unknown(UnknownProperty { ident, value }) => {
format!("{}[{}]", ident, value)
}
}
}
}
pub fn parse_collection<'a, E: nom::error::ParseError<&'a str>>(
input: &'a str,
) -> IResult<&'a str, Vec<Tree>, E> {
let (input, roots) = separated_list1(multispace1, parse_tree)(input)?;
let trees = roots
.into_iter()
.map(|root| Tree { root })
.collect::<Vec<Tree>>();
Ok((input, trees))
}
// 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, Node, E> {
let (input, _) = multispace0(input)?;
let (input, _) = tag("(")(input)?;
let (input, node) = parse_node(input)?;
let (input, _) = multispace0(input)?;
let (input, _) = tag(")")(input)?;
Ok((input, node))
}
fn parse_node<'a, E: nom::error::ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, Node, E> {
let (input, _) = multispace0(input)?;
let (input, _) = opt(tag(";"))(input)?;
let (input, properties) = many1(parse_property)(input)?;
let (input, next) = opt(parse_node)(input)?;
let (input, mut next_seq) = many0(parse_tree)(input)?;
let mut next = next.map(|n| vec![n]).unwrap_or(vec![]);
next.append(&mut next_seq);
Ok((input, Node { properties, next }))
}
fn parse_property<'a, E: nom::error::ParseError<&'a str>>(
input: &'a str,
) -> IResult<&'a str, Property, E> {
let (input, _) = multispace0(input)?;
let (input, ident) = alpha1(input)?;
let (input, prop) = match ident {
"W" => parse_propval(parse_move(Color::White))(input)?,
"B" => parse_propval(parse_move(Color::Black))(input)?,
"C" => parse_propval(parse_comment())(input)?,
"WL" => parse_propval(parse_time_left(Color::White))(input)?,
"BL" => parse_propval(parse_time_left(Color::Black))(input)?,
"BM" => discard_propval()
.map(|_| Property::Annotation(Annotation::BadMove))
.parse(input)?,
"DO" => discard_propval()
.map(|_| Property::Annotation(Annotation::DoubtfulMove))
.parse(input)?,
"IT" => discard_propval()
.map(|_| Property::Annotation(Annotation::InterestingMove))
.parse(input)?,
"TE" => discard_propval()
.map(|_| Property::Annotation(Annotation::Tesuji))
.parse(input)?,
"AP" => parse_propval(parse_simple_text().map(Property::Application))(input)?,
"CA" => parse_propval(parse_simple_text().map(Property::Charset))(input)?,
"FF" => parse_propval(parse_number().map(Property::FileFormat))(input)?,
"GM" => unimplemented!(),
"ST" => unimplemented!(),
"SZ" => unimplemented!(),
"DM" => discard_propval()
.map(|_| Property::Evaluation(Evaluation::Even))
.parse(input)?,
"GB" => discard_propval()
.map(|_| Property::Evaluation(Evaluation::GoodForBlack))
.parse(input)?,
"GW" => discard_propval()
.map(|_| Property::Evaluation(Evaluation::GoodForWhite))
.parse(input)?,
"UC" => discard_propval()
.map(|_| Property::Evaluation(Evaluation::Unclear))
.parse(input)?,
"V" => unimplemented!(),
"AN" => parse_propval(parse_simple_text().map(Property::Annotator))(input)?,
"BR" => parse_propval(parse_simple_text().map(Property::BlackRank))(input)?,
"BT" => parse_propval(parse_simple_text().map(Property::BlackTeam))(input)?,
"CP" => parse_propval(parse_simple_text().map(Property::Copyright))(input)?,
"DT" => unimplemented!(),
"EV" => parse_propval(parse_simple_text().map(Property::EventName))(input)?,
"GN" => parse_propval(parse_simple_text().map(Property::GameName))(input)?,
"GC" => parse_propval(parse_simple_text().map(Property::ExtraGameInformation))(input)?,
"ON" => parse_propval(parse_simple_text().map(Property::GameOpening))(input)?,
"OT" => parse_propval(parse_simple_text().map(Property::Overtime))(input)?,
"PB" => parse_propval(parse_simple_text().map(Property::BlackPlayer))(input)?,
"PC" => parse_propval(parse_simple_text().map(Property::GameLocation))(input)?,
"PW" => parse_propval(parse_simple_text().map(Property::WhitePlayer))(input)?,
"RE" => unimplemented!(),
"RO" => parse_propval(parse_simple_text().map(Property::Round))(input)?,
"RU" => parse_propval(parse_simple_text().map(Property::Ruleset))(input)?,
"SO" => parse_propval(parse_simple_text().map(Property::Source))(input)?,
"TM" => unimplemented!(),
"US" => parse_propval(parse_simple_text().map(Property::User))(input)?,
"WR" => parse_propval(parse_simple_text().map(Property::WhiteRank))(input)?,
"WT" => parse_propval(parse_simple_text().map(Property::WhiteTeam))(input)?,
_ => parse_propval(parse_simple_text().map(|value| {
Property::Unknown(UnknownProperty {
ident: ident.to_owned(),
value,
})
}))(input)?,
};
Ok((input, prop))
}
fn parse_comment<'a, E: nom::error::ParseError<&'a str>>() -> impl Parser<&'a str, Property, E> {
parse_text().map(|text| Property::Comment(text))
}
fn parse_move<'a, E: nom::error::ParseError<&'a str>>(
color: Color,
) -> impl FnMut(&'a str) -> IResult<&'a str, Property, E> {
{
let color = color.clone();
move |input: &'a str| {
take_until1("]")
.map(|text: &'a str| Property::Move((color.clone(), text.to_owned())))
.parse(input)
}
}
}
fn parse_time_left<'a, E: ParseError<&'a str>>(
color: Color,
) -> impl FnMut(&'a str) -> IResult<&'a str, Property, E> {
{
let color = color.clone();
move |input: &'a str| {
let (input, value) = parse_real().parse(input)?;
let (input, _) = tag("]")(input)?;
Ok((
input,
Property::TimeLeft((color.clone(), Duration::from_secs(value as u64))),
))
}
}
}
fn parse_propval<'a, E: nom::error::ParseError<&'a str>>(
mut parser: impl Parser<&'a str, Property, E>,
) -> impl FnMut(&'a str) -> IResult<&'a str, Property, E> {
move |input| {
let (input, _) = multispace0(input)?;
let (input, _) = tag("[")(input)?;
let (input, value) = parser.parse(input)?;
let (input, _) = tag("]")(input)?;
Ok((input, value))
}
}
fn discard_propval<'a, E: nom::error::ParseError<&'a str>>() -> impl Parser<&'a str, (), E> {
|input| {
let (input, _) = multispace0(input)?;
let (input, _) = tag("[")(input)?;
let (input, _) = parse_text().parse(input)?;
let (input, _) = tag("]")(input)?;
Ok((input, ()))
}
}
fn parse_number<'a, E: ParseError<&'a str>>() -> impl Parser<&'a str, i32, E> {
|input| {
let (input, sign) = opt(alt((tag("+"), tag("-"))))(input)?;
let (input, value) = digit1(input)?;
let mult = if sign == Some("-") { -1 } else { 1 };
Ok((input, value.parse::<i32>().unwrap() * mult))
}
}
fn parse_real<'a, E: ParseError<&'a str>>() -> impl Parser<&'a str, f32, E> {
|input| unimplemented!()
}
fn parse_double<'a, E: ParseError<&'a str>>() -> impl Parser<&'a str, Double, E> {
|input| unimplemented!()
}
fn parse_simple_text<'a, E: ParseError<&'a str>>() -> impl Parser<&'a str, String, E> {
|input| unimplemented!()
}
fn parse_text<'a, E: ParseError<&'a str>>() -> impl Parser<&'a str, String, E> {
|input| {
let (input, value) = opt(escaped_transform(
none_of("\\]"),
'\\',
alt((
value("]", tag("]")),
value("\\", tag("\\")),
value("", tag("\n")),
)),
))(input)?;
Ok((input, value.unwrap_or("".to_owned())))
}
}
#[cfg(test)]
mod test {
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::<nom::error::VerboseError<&str>>("C[a]").unwrap();
assert_eq!(prop, Property::Comment("a".to_owned()));
}
#[test]
fn it_can_parse_a_standalone_node() {
let (_, node) = parse_node::<nom::error::VerboseError<&str>>(";B[ab]").unwrap();
assert_eq!(
node,
Node {
properties: vec![Property::Move((Color::Black, "ab".to_owned()))],
next: vec![]
}
);
let (_, node) =
parse_node::<nom::error::VerboseError<&str>>(";B[ab];W[dp];B[pq]C[some comments]")
.unwrap();
assert_eq!(
node,
Node {
properties: vec![Property::Move((Color::Black, "ab".to_owned()))],
next: vec![Node {
properties: vec![Property::Move((Color::White, "dp".to_owned()))],
next: vec![Node {
properties: vec![
Property::Move((Color::Black, "pq".to_owned())),
Property::Comment("some comments".to_owned())
],
next: vec![],
}]
}]
}
);
}
#[test]
fn it_can_parse_a_simple_sequence() {
let (_, sequence) =
parse_tree::<nom::error::VerboseError<&str>>("(;B[ab];W[dp];B[pq]C[some comments])")
.unwrap();
assert_eq!(
sequence,
Node {
properties: vec![Property::Move((Color::Black, "ab".to_owned()))],
next: vec![Node {
properties: vec![Property::Move((Color::White, "dp".to_owned()))],
next: vec![Node {
properties: vec![
Property::Move((Color::Black, "pq".to_owned())),
Property::Comment("some comments".to_owned())
],
next: vec![],
}]
}],
},
);
}
#[test]
fn it_can_parse_a_branching_sequence() {
let text = "(;C[a];C[b](;C[c])(;C[d];C[e]))";
let (_, tree) = parse_tree::<nom::error::VerboseError<&str>>(text).unwrap();
let expected = Node {
properties: vec![Property::Comment("a".to_owned())],
next: vec![Node {
properties: vec![Property::Comment("b".to_owned())],
next: vec![
Node {
properties: vec![Property::Comment("c".to_owned())],
next: vec![],
},
Node {
properties: vec![Property::Comment("d".to_owned())],
next: vec![Node {
properties: vec![Property::Comment("e".to_owned())],
next: vec![],
}],
},
],
}],
};
assert_eq!(tree, expected);
}
#[test]
fn it_can_parse_example_1() {
let (_, tree) = parse_tree::<nom::error::VerboseError<&str>>(EXAMPLE).unwrap();
let j = Node {
properties: vec![Property::Comment("j".to_owned())],
next: vec![],
};
let i = Node {
properties: vec![Property::Comment("i".to_owned())],
next: vec![],
};
let h = Node {
properties: vec![Property::Comment("h".to_owned())],
next: vec![i],
};
let g = Node {
properties: vec![Property::Comment("g".to_owned())],
next: vec![h],
};
let f = Node {
properties: vec![Property::Comment("f".to_owned())],
next: vec![g, j],
};
let e = Node {
properties: vec![Property::Comment("e".to_owned())],
next: vec![],
};
let d = Node {
properties: vec![Property::Comment("d".to_owned())],
next: vec![e],
};
let c = Node {
properties: vec![Property::Comment("c".to_owned())],
next: vec![],
};
let b = Node {
properties: vec![Property::Comment("b".to_owned())],
next: vec![c, d],
};
let a = Node {
properties: vec![Property::Comment("a".to_owned())],
next: vec![b],
};
let expected = Node {
properties: vec![
Property::FileFormat(4),
Property::Comment("root".to_owned()),
],
next: vec![a, f],
};
assert_eq!(tree, expected);
}
#[test]
fn it_can_regenerate_the_tree() {
let (_, tree1) = parse_tree::<nom::error::VerboseError<&str>>(EXAMPLE).unwrap();
let tree1 = Tree { root: tree1 };
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::<nom::error::VerboseError<&str>>(&tree1.to_string()).unwrap();
assert_eq!(tree1, Tree { root: tree2 });
}
#[test]
fn it_parses_propvals() {
let (_, propval) = parse_propval::<nom::error::VerboseError<&str>>(parse_comment())
.parse("[]")
.unwrap();
assert_eq!(propval, Property::Comment("".to_owned()));
let (_, propval) = parse_propval::<nom::error::VerboseError<&str>>(parse_comment())
.parse("[normal propval]")
.unwrap();
assert_eq!(propval, Property::Comment("normal propval".to_owned()));
let (_, propval) = parse_propval::<nom::error::VerboseError<&str>>(parse_comment())
.parse(r"[need an [escape\] in the propval]")
.unwrap();
assert_eq!(
propval,
Property::Comment("need an [escape] in the propval".to_owned())
);
}
#[test]
fn it_parses_propvals_with_hard_linebreaks() {
let (_, propval) = parse_text::<nom::error::VerboseError<&str>>()
.parse(
"There are hard linebreaks & soft linebreaks.
Soft linebreaks...",
)
.unwrap();
assert_eq!(
propval,
"There are hard linebreaks & soft linebreaks.
Soft linebreaks..."
);
}
#[test]
fn it_parses_propvals_with_escaped_closing_brackets() {
let (_, propval) = parse_text::<nom::error::VerboseError<&str>>()
.parse(r"escaped closing \] bracket")
.unwrap();
assert_eq!(propval, r"escaped closing ] bracket".to_owned());
}
#[test]
fn it_parses_propvals_with_soft_linebreaks() {
let (_, propval) = parse_text::<nom::error::VerboseError<&str>>()
.parse(
r"Soft linebreaks are linebreaks preceeded by '\\' like this one >o\
k<. Hard line breaks are all other linebreaks.",
)
.unwrap();
assert_eq!(
propval,
"Soft linebreaks are linebreaks preceeded by '\\' like this one >ok<. Hard line breaks are all other linebreaks."
);
}
#[test]
fn it_parses_sgf_with_newline_in_sequence() {
let data = String::from(
"(;FF[4]C[root](;C[a];C[b](;C[c])(;C[d];C[e]
))(;C[f](;C[g];C[h];C[i])(;C[j])))",
);
parse_tree::<nom::error::VerboseError<&str>>(&data).unwrap();
}
#[test]
fn it_parses_sgf_with_newline_between_two_sequence_closings() {
let data = String::from(
"(;FF[4]C[root](;C[a];C[b](;C[c])(;C[d];C[e])
)(;C[f](;C[g];C[h];C[i])(;C[j])))",
);
parse_tree::<nom::error::VerboseError<&str>>(&data).unwrap();
}
}