├── .gitignore ├── fuzz ├── .gitignore ├── fuzz_targets │ └── parse.rs └── Cargo.toml ├── src ├── props.rs ├── props │ ├── error.rs │ ├── to_sgf.rs │ ├── sgf_prop.rs │ ├── parse.rs │ └── values.rs ├── serialize.rs ├── lib.rs ├── unknown_game.rs ├── game_tree.rs ├── lexer.rs ├── go.rs ├── sgf_node.rs ├── parser.rs └── prop_macro.rs ├── .github └── workflows │ ├── release.yml │ └── ci.yml ├── Cargo.toml ├── LICENSE ├── README.md └── resources └── test └── ff4_ex.sgf /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /fuzz/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | target 3 | corpus 4 | artifacts 5 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/parse.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | use libfuzzer_sys::fuzz_target; 3 | extern crate sgf_parse; 4 | 5 | fuzz_target!(|data: &[u8]| { 6 | if let Ok(s) = std::str::from_utf8(data) { 7 | let _ = sgf_parse::parse(s); 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /src/props.rs: -------------------------------------------------------------------------------- 1 | mod error; 2 | pub mod parse; 3 | mod sgf_prop; 4 | mod to_sgf; 5 | mod values; 6 | 7 | pub use error::SgfPropError; 8 | pub use sgf_prop::SgfProp; 9 | pub use to_sgf::ToSgf; 10 | pub use values::{Color, Double, PropertyType, SimpleText, Text}; 11 | -------------------------------------------------------------------------------- /src/props/error.rs: -------------------------------------------------------------------------------- 1 | // Error type for invalid SGF properties. 2 | #[derive(Debug)] 3 | pub struct SgfPropError {} 4 | 5 | impl std::fmt::Display for SgfPropError { 6 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 7 | write!(f, "Invalid property value") 8 | } 9 | } 10 | 11 | impl std::error::Error for SgfPropError {} 12 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create Release 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | 7 | jobs: 8 | release: 9 | name: Create release 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Create release 13 | uses: softprops/action-gh-release@v1 14 | with: 15 | tag_name: ${{ github.ref_name }} 16 | draft: true 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sgf-parse" 3 | version = "4.2.8" 4 | edition = "2018" 5 | authors = ["Julian Andrews "] 6 | license = "MIT" 7 | keywords = ["baduk", "parser", "sgf", "go"] 8 | repository = "https://github.com/julianandrews/sgf-parse/" 9 | readme = "README.md" 10 | description = "A parser for the SGF file format for Go games" 11 | documentation = "https://docs.rs/sgf-parse" 12 | categories = ["data-structures", "parsing"] 13 | -------------------------------------------------------------------------------- /fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | 2 | [package] 3 | name = "sgf-parse-fuzz" 4 | version = "0.0.0" 5 | authors = ["Automatically generated"] 6 | publish = false 7 | edition = "2018" 8 | 9 | [package.metadata] 10 | cargo-fuzz = true 11 | 12 | [dependencies] 13 | libfuzzer-sys = "0.3" 14 | 15 | [dependencies.sgf-parse] 16 | path = ".." 17 | 18 | # Prevent this from interfering with workspaces 19 | [workspace] 20 | members = ["."] 21 | 22 | [[bin]] 23 | name = "parse" 24 | path = "fuzz_targets/parse.rs" 25 | test = false 26 | doc = false 27 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - '*' 5 | pull_request: {} 6 | 7 | name: Continuous integration 8 | 9 | jobs: 10 | check: 11 | name: Check 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: dtolnay/rust-toolchain@stable 16 | - run: cargo check 17 | 18 | test: 19 | name: Test Suite 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: dtolnay/rust-toolchain@stable 24 | - run: cargo test --all-features 25 | 26 | fmt: 27 | name: Rustfmt 28 | runs-on: ubuntu-latest 29 | steps: 30 | - uses: actions/checkout@v4 31 | - uses: dtolnay/rust-toolchain@stable 32 | with: 33 | components: rustfmt 34 | - run: cargo fmt --all -- --check 35 | 36 | clippy: 37 | name: Clippy 38 | runs-on: ubuntu-latest 39 | steps: 40 | - uses: actions/checkout@v4 41 | - uses: dtolnay/rust-toolchain@stable 42 | with: 43 | components: clippy 44 | - run: cargo clippy -- -D warnings 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [year] [fullname] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sgf-parse - A library for parsing SGF files 2 | 3 | ![Continuous integration](https://github.com/julianandrews/sgf-parse/workflows/Continuous%20integration/badge.svg) 4 | 5 | A library for parsing [SGF FF\[4\]](https://www.red-bean.com/sgf/sgf4.html) 6 | files in Rust. 7 | 8 | `sgf-parse` provides a reliable but simple structured, standard-compliant 9 | interface for reading and writing `.sgf` files. For all standard SGF data types, 10 | properties are validated and parsed into appropriate native Rust types. 11 | Non-standard properties are parsed and preserved. 12 | 13 | [Documentation](https://docs.rs/sgf-parse) 14 | 15 | ## Installation 16 | Find `sgf-parse` on [crates.io](https://crates.io/crates/sgf-parse) 17 | 18 | ## Contributing 19 | Pull requests are welcome. For major changes, please open an issue first to 20 | discuss what you would like to change. 21 | 22 | I would be particularly interested in any PRs to add support for non-Go games. 23 | Right now `sgf-parse` in principle can support any games supported by SGF, but 24 | I've only got specific implementations for Go, and a catchall with no special 25 | behavior where moves, stones, and points are just strings left to the library 26 | user to interpret. 27 | -------------------------------------------------------------------------------- /src/serialize.rs: -------------------------------------------------------------------------------- 1 | use crate::GameTree; 2 | 3 | /// Returns the serialized SGF text from a collection of [`GameTree`] objects. 4 | /// 5 | /// For serializing a single node, check out the 6 | /// [`SgfNode::serialize`](`crate::SgfNode::serialize`) method. 7 | /// 8 | /// # Examples 9 | /// ``` 10 | /// use sgf_parse::{serialize, SgfNode, SgfProp}; 11 | /// use sgf_parse::go::Prop; 12 | /// 13 | /// let first_node: SgfNode:: = { 14 | /// let children = vec![ 15 | /// SgfNode::new( 16 | /// vec![Prop::new("B".to_string(), 17 | /// vec!["dd".to_string()])], vec![], 18 | /// false, 19 | /// ), 20 | /// ]; 21 | /// SgfNode::new(vec![Prop::SZ((19, 19))], children, true) 22 | /// }; 23 | /// let second_node = SgfNode::::new(vec![Prop::C("A comment".into())], vec![], true); 24 | /// let gametrees = vec![first_node.into(), second_node.into()]; 25 | /// let serialized = serialize(&gametrees); 26 | /// 27 | /// assert_eq!(serialized, "(;SZ[19:19];B[dd])(;C[A comment])"); 28 | /// ``` 29 | pub fn serialize<'a>(gametrees: impl IntoIterator) -> String { 30 | gametrees 31 | .into_iter() 32 | .map(|gametree| gametree.to_string()) 33 | .collect::>() 34 | .join("") 35 | } 36 | 37 | #[cfg(test)] 38 | mod test { 39 | use super::serialize; 40 | use crate::parse; 41 | 42 | #[test] 43 | fn simple_sgf() { 44 | let sgf = "(;C[Some comment];B[de]FOO[bar][baz];W[fe])(;B[de];W[ff])"; 45 | let game_trees = parse(sgf).unwrap(); 46 | let result = serialize(&game_trees); 47 | assert_eq!(result, sgf); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Data structures and utilities for parsing [SGF FF\[4\] files](https://www.red-bean.com/sgf/). 2 | //! 3 | //! ## Quick Start 4 | //! 5 | //! Most common use case - parsing a Go game and iterating through its moves: 6 | //! ```rust 7 | //! use sgf_parse::{parse, go::Prop, go::Move}; 8 | //! 9 | //! let sgf = "(;FF[4]GM[1]B[aa];W[ab])"; 10 | //! 11 | //! let collection = parse(sgf).unwrap(); 12 | //! let root_node = collection.first().unwrap().as_go_node().unwrap(); 13 | //! 14 | //! // Iterate through the main variation 15 | //! for node in root_node.main_variation() { 16 | //! if let Some(prop) = node.get_move() { 17 | //! println!("Move: {}", prop); 18 | //! } 19 | //! } 20 | //! ``` 21 | //! 22 | //! Working with multi-game collections: 23 | //! ```rust 24 | //! # use sgf_parse::parse; 25 | //! let sgf = "(;FF[4]GM[1];B[aa])(;FF[4]GM[1];B[dd])"; 26 | //! let collection = parse(sgf).unwrap(); 27 | //! 28 | //! for gametree in &collection { 29 | //! let root_node = gametree.as_go_node().unwrap(); 30 | //! println!("Game has {} nodes", root_node.main_variation().count()); 31 | //! } 32 | //! ``` 33 | //! 34 | //! For reading SGFs your starting point will likely be [`go::parse`]. For parsing non-go games 35 | //! check out the [`parse`](`parse()`) function. 36 | //! 37 | //! For writing SGFs check out [`SgfNode::serialize`] for writing single game trees or 38 | //! [`serialize`](`serialize()`) for writing whole collections. 39 | 40 | #[macro_use] 41 | mod prop_macro; 42 | 43 | pub mod go; 44 | pub mod unknown_game; 45 | 46 | mod game_tree; 47 | mod lexer; 48 | mod parser; 49 | mod props; 50 | mod serialize; 51 | mod sgf_node; 52 | 53 | pub use game_tree::{GameTree, GameType}; 54 | pub use lexer::LexerError; 55 | pub use parser::{parse, parse_with_options, ParseOptions, SgfParseError}; 56 | pub use props::{Color, Double, PropertyType, SgfProp, SimpleText, Text}; 57 | pub use serialize::serialize; 58 | pub use sgf_node::{InvalidNodeError, SgfNode}; 59 | -------------------------------------------------------------------------------- /src/props/to_sgf.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use crate::{Color, Double, SimpleText, Text}; 4 | 5 | pub trait ToSgf { 6 | fn to_sgf(&self) -> String; 7 | } 8 | 9 | impl ToSgf for Vec { 10 | fn to_sgf(&self) -> String { 11 | self.join("][") 12 | } 13 | } 14 | 15 | impl ToSgf for HashSet

{ 16 | fn to_sgf(&self) -> String { 17 | self.iter() 18 | .map(|x| x.to_sgf()) 19 | .collect::>() 20 | .join("][") 21 | } 22 | } 23 | 24 | impl ToSgf for (A, B) { 25 | fn to_sgf(&self) -> String { 26 | format!("{}:{}", self.0.to_sgf(), self.1.to_sgf()) 27 | } 28 | } 29 | 30 | impl ToSgf for Option { 31 | fn to_sgf(&self) -> String { 32 | match self { 33 | None => "".to_string(), 34 | Some(x) => x.to_sgf(), 35 | } 36 | } 37 | } 38 | 39 | impl ToSgf for u8 { 40 | fn to_sgf(&self) -> String { 41 | self.to_string() 42 | } 43 | } 44 | 45 | impl ToSgf for i64 { 46 | fn to_sgf(&self) -> String { 47 | self.to_string() 48 | } 49 | } 50 | 51 | impl ToSgf for f64 { 52 | fn to_sgf(&self) -> String { 53 | self.to_string() 54 | } 55 | } 56 | 57 | impl ToSgf for Double { 58 | fn to_sgf(&self) -> String { 59 | match self { 60 | Self::One => "1".to_string(), 61 | Self::Two => "2".to_string(), 62 | } 63 | } 64 | } 65 | 66 | impl ToSgf for Color { 67 | fn to_sgf(&self) -> String { 68 | match self { 69 | Self::Black => "B".to_string(), 70 | Self::White => "W".to_string(), 71 | } 72 | } 73 | } 74 | 75 | impl ToSgf for Text { 76 | fn to_sgf(&self) -> String { 77 | escape_string(&self.text) 78 | } 79 | } 80 | 81 | impl ToSgf for SimpleText { 82 | fn to_sgf(&self) -> String { 83 | escape_string(&self.text) 84 | } 85 | } 86 | 87 | fn escape_string(s: &str) -> String { 88 | s.replace('\\', "\\\\") 89 | .replace(']', "\\]") 90 | .replace(':', "\\:") 91 | } 92 | -------------------------------------------------------------------------------- /src/unknown_game.rs: -------------------------------------------------------------------------------- 1 | //! Generic types for SGFs without a known game. 2 | //! 3 | //! This module contains a generic [`SgfProp`] implementation appropriate 4 | //! for use with any SGF file. This implementation recognizes all [general 5 | //! properties](https://www.red-bean.com/sgf/properties.html), but any game 6 | //! specific property will parse as [`Prop::Unknown`]. 7 | //! 8 | //! SGF Move, Point, and Stone values are all simply stored as strings. 9 | 10 | use crate::props::parse::FromCompressedList; 11 | use crate::props::{PropertyType, SgfPropError, ToSgf}; 12 | use crate::{InvalidNodeError, SgfProp}; 13 | use std::collections::HashSet; 14 | 15 | sgf_prop! { 16 | Prop, String, String, String, 17 | { } 18 | } 19 | 20 | /// An SGF [Point](https://www.red-bean.com/sgf/go.html#types) value for an unknown game. 21 | pub type Point = String; 22 | 23 | /// An SGF [Stone](https://www.red-bean.com/sgf/go.html#types) value for an unknown game. 24 | pub type Stone = String; 25 | 26 | /// An SGF [Move](https://www.red-bean.com/sgf/go.html#types) value for an unknown game. 27 | pub type Move = String; 28 | 29 | impl SgfProp for Prop { 30 | type Point = Point; 31 | type Stone = Stone; 32 | type Move = Move; 33 | 34 | fn new(identifier: String, values: Vec) -> Self { 35 | Self::parse_general_prop(identifier, values) 36 | } 37 | 38 | fn identifier(&self) -> String { 39 | match self.general_identifier() { 40 | Some(identifier) => identifier, 41 | None => panic!("Unimplemented identifier for {:?}", self), 42 | } 43 | } 44 | 45 | fn property_type(&self) -> Option { 46 | self.general_property_type() 47 | } 48 | 49 | fn validate_properties(properties: &[Self], is_root: bool) -> Result<(), InvalidNodeError> { 50 | Self::general_validate_properties(properties, is_root) 51 | } 52 | } 53 | 54 | impl std::fmt::Display for Prop { 55 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 56 | let prop_string = match self.serialize_prop_value() { 57 | Some(s) => s, 58 | None => panic!("Unimplemented identifier for {:?}", self), 59 | }; 60 | write!(f, "{}[{}]", self.identifier(), prop_string) 61 | } 62 | } 63 | 64 | impl FromCompressedList for String { 65 | fn from_compressed_list(ul: &Self, lr: &Self) -> Result, SgfPropError> { 66 | // For an unknown game we have no way to parse a compressed list, but since points 67 | // are just strings we can just return a single point with that string and let the 68 | // user decide what to do with it. 69 | let mut points = HashSet::new(); 70 | points.insert(format!("{ul}:{lr}")); 71 | Ok(points) 72 | } 73 | } 74 | 75 | impl ToSgf for String { 76 | fn to_sgf(&self) -> String { 77 | self.to_owned() 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/props/sgf_prop.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Debug, Display}; 2 | 3 | use super::{PropertyType, ToSgf}; 4 | use crate::InvalidNodeError; 5 | 6 | /// A type that can be used for properties in an [`SgfNode`](`crate::SgfNode`). 7 | /// 8 | /// This trait is sealed and cannot be implemented for types outside of `sgf_parse`. 9 | pub trait SgfProp: Debug + Display + Sized + Clone + Eq + private::Sealed { 10 | type Point: Debug + Clone + PartialEq + Eq + std::hash::Hash + ToSgf; 11 | type Stone: Debug + Clone + PartialEq + Eq + std::hash::Hash + ToSgf; 12 | type Move: Debug + Clone + PartialEq + Eq + ToSgf; 13 | 14 | /// Returns a new property parsed from the provided identifier and values 15 | /// 16 | /// # Examples 17 | /// ``` 18 | /// use sgf_parse::SgfProp; 19 | /// use sgf_parse::go::Prop; 20 | /// 21 | /// // Prop::B(Point{ x: 2, y: 3 } 22 | /// let prop = Prop::new("B".to_string(), vec!["cd".to_string()]); 23 | /// // Prop::AB(vec![Point{ x: 2, y: 3 }, Point { x: 3, y: 3 }]) 24 | /// let prop = Prop::new("AB".to_string(), vec!["cd".to_string(), "dd".to_string()]); 25 | /// // Prop::Unknown("FOO", vec!["Text"]) 26 | /// let prop = Prop::new("FOO".to_string(), vec!["Text".to_string()]); 27 | /// ``` 28 | fn new(identifier: String, values: Vec) -> Self; 29 | 30 | /// Returns a the identifier associated with the [`SgfProp`]. 31 | /// 32 | /// # Examples 33 | /// ``` 34 | /// use sgf_parse::SgfProp; 35 | /// use sgf_parse::go::Prop; 36 | /// 37 | /// let prop = Prop::new("W".to_string(), vec!["de".to_string()]); 38 | /// assert_eq!(prop.identifier(), "W"); 39 | /// let prop = Prop::new("FOO".to_string(), vec!["de".to_string()]); 40 | /// assert_eq!(prop.identifier(), "FOO"); 41 | /// ``` 42 | fn identifier(&self) -> String; 43 | 44 | /// Returns the [`PropertyType`] associated with the property. 45 | /// 46 | /// # Examples 47 | /// ``` 48 | /// use sgf_parse::{PropertyType, SgfProp}; 49 | /// use sgf_parse::go::Prop; 50 | /// 51 | /// let prop = Prop::new("W".to_string(), vec!["de".to_string()]); 52 | /// assert_eq!(prop.property_type(), Some(PropertyType::Move)); 53 | /// let prop = Prop::new("FOO".to_string(), vec!["de".to_string()]); 54 | /// assert_eq!(prop.property_type(), None); 55 | /// ``` 56 | fn property_type(&self) -> Option; 57 | 58 | /// Validates a set of properties. 59 | /// 60 | /// # Errors 61 | /// Returns an error if the collection of properties isn't valid. 62 | fn validate_properties(properties: &[Self], is_root: bool) -> Result<(), InvalidNodeError>; 63 | } 64 | 65 | // Prevent users from implementing the SgfProp trait. 66 | // Because `parse` has to return an enum, with the current design, implementing 67 | // a new game outside the crate is a mess. 68 | // 69 | // If you'd like to implement this trait for a new game, PR's are very welcome! 70 | mod private { 71 | pub trait Sealed {} 72 | impl Sealed for crate::go::Prop {} 73 | impl Sealed for crate::unknown_game::Prop {} 74 | impl Sealed for &T where T: ?Sized + Sealed {} 75 | } 76 | -------------------------------------------------------------------------------- /src/props/parse.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | use std::hash::Hash; 3 | use std::str::FromStr; 4 | 5 | use super::SgfPropError; 6 | 7 | pub trait FromCompressedList: Sized { 8 | fn from_compressed_list( 9 | upper_left: &Self, 10 | lower_right: &Self, 11 | ) -> Result, SgfPropError>; 12 | } 13 | 14 | pub fn parse_single_value(values: &[String]) -> Result { 15 | if values.len() != 1 { 16 | return Err(SgfPropError {}); 17 | } 18 | values[0].parse().map_err(|_| SgfPropError {}) 19 | } 20 | 21 | pub fn parse_tuple(value: &str) -> Result<(T1, T2), SgfPropError> { 22 | let (s1, s2) = split_compose(value)?; 23 | Ok(( 24 | s1.parse().map_err(|_| SgfPropError {})?, 25 | s2.parse().map_err(|_| SgfPropError {})?, 26 | )) 27 | } 28 | 29 | pub fn parse_elist( 30 | values: &[String], 31 | ) -> Result, SgfPropError> { 32 | let mut elements = HashSet::new(); 33 | for value in values { 34 | if value.contains(':') { 35 | let (upper_left, lower_right): (T, T) = parse_tuple(value)?; 36 | elements.extend(T::from_compressed_list(&upper_left, &lower_right)?); 37 | } else { 38 | let item = value.parse().map_err(|_| SgfPropError {})?; 39 | elements.insert(item); 40 | } 41 | } 42 | Ok(elements) 43 | } 44 | 45 | pub fn parse_list( 46 | values: &[String], 47 | ) -> Result, SgfPropError> { 48 | let points = parse_elist::(values)?; 49 | if points.is_empty() { 50 | return Err(SgfPropError {}); 51 | } 52 | 53 | Ok(points) 54 | } 55 | 56 | pub fn parse_list_composed( 57 | values: &[String], 58 | ) -> Result, SgfPropError> { 59 | let mut pairs = HashSet::new(); 60 | for value in values.iter() { 61 | let pair = parse_tuple(value)?; 62 | if pair.0 == pair.1 || pairs.contains(&pair) { 63 | return Err(SgfPropError {}); 64 | } 65 | pairs.insert(pair); 66 | } 67 | 68 | Ok(pairs) 69 | } 70 | 71 | pub fn split_compose(value: &str) -> Result<(&str, &str), SgfPropError> { 72 | let parts: Vec<&str> = value.split(':').collect(); 73 | if parts.len() != 2 { 74 | return Err(SgfPropError {}); 75 | } 76 | 77 | Ok((parts[0], parts[1])) 78 | } 79 | 80 | pub fn verify_empty(values: &[String]) -> Result<(), SgfPropError> { 81 | if !(values.is_empty() || (values.len() == 1 && values[0].is_empty())) { 82 | return Err(SgfPropError {}); 83 | } 84 | Ok(()) 85 | } 86 | 87 | #[cfg(test)] 88 | mod test { 89 | use super::parse_list; 90 | use crate::go::Point; 91 | use std::collections::HashSet; 92 | 93 | #[test] 94 | pub fn parse_list_point() { 95 | let values = vec!["pq:ss".to_string(), "so".to_string(), "lr:ns".to_string()]; 96 | let expected: HashSet<_> = vec![ 97 | (15, 16), 98 | (16, 16), 99 | (17, 16), 100 | (18, 16), 101 | (15, 17), 102 | (16, 17), 103 | (17, 17), 104 | (18, 17), 105 | (15, 18), 106 | (16, 18), 107 | (17, 18), 108 | (18, 18), 109 | (18, 14), 110 | (11, 17), 111 | (12, 17), 112 | (13, 17), 113 | (11, 18), 114 | (12, 18), 115 | (13, 18), 116 | ] 117 | .into_iter() 118 | .map(|(x, y)| Point { x, y }) 119 | .collect(); 120 | 121 | let result: HashSet<_> = parse_list::(&values).unwrap(); 122 | 123 | assert_eq!(result, expected); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/game_tree.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | 3 | use crate::{go, unknown_game, SgfNode, SgfParseError}; 4 | 5 | /// The game recorded in a [`GameTree`]. 6 | /// 7 | /// Any [`GameTree`] retured by [`parse`](`crate::parse`) will have a game type which corresponds to 8 | /// the SGF `GM` property of the root node. 9 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 10 | pub enum GameType { 11 | Go, 12 | Unknown, 13 | } 14 | 15 | /// An SGF [GameTree](https://www.red-bean.com/sgf/sgf4.html#ebnf-def) value. 16 | /// 17 | /// This type allows creating a collection of [`SgfNode`] values for different games. This is 18 | /// used in the return type of the [`parse`](`crate::parse()`) function. Users of the 19 | /// [`serialize`](`crate::serialize()`) function will need to build these. 20 | /// 21 | /// For now, all non-Go games will parse as [`GameTree::Unknown`] which should also be used for any 22 | /// serialization of non-Go games. 23 | #[derive(Clone, Debug, PartialEq)] 24 | pub enum GameTree { 25 | GoGame(SgfNode), 26 | Unknown(SgfNode), 27 | } 28 | 29 | impl GameTree { 30 | /// Consumes a Go game `GameTree` and returns the contained [`SgfNode`]. 31 | /// 32 | /// This is a convenience method for go games. 33 | /// 34 | /// # Errors 35 | /// Returns an error if the variant isn't a [`GameTree::GoGame`]. 36 | /// 37 | /// # Examples 38 | /// ``` 39 | /// use sgf_parse::parse; 40 | /// 41 | /// let gametree = parse("(;B[de]C[A comment])").unwrap().into_iter().next().unwrap(); 42 | /// let sgf_node = gametree.into_go_node().unwrap(); 43 | /// ``` 44 | pub fn into_go_node(self) -> Result, SgfParseError> { 45 | match self { 46 | Self::GoGame(sgf_node) => Ok(sgf_node), 47 | _ => Err(SgfParseError::UnexpectedGameType), 48 | } 49 | } 50 | 51 | /// Return a reference to the root [`SgfNode`] of the tree. 52 | /// 53 | /// This is a convenience method for go games. 54 | /// 55 | /// # Errors 56 | /// Returns an error if the variant isn't a [`GameTree::GoGame`]. 57 | /// 58 | /// # Examples 59 | /// ``` 60 | /// use sgf_parse::parse; 61 | /// 62 | /// let gametrees = parse("(;B[de]C[A comment])").unwrap(); 63 | /// let sgf_node = gametrees.first().unwrap().as_go_node().unwrap(); 64 | /// ``` 65 | pub fn as_go_node(&self) -> Result<&'_ SgfNode, SgfParseError> { 66 | match self { 67 | Self::GoGame(sgf_node) => Ok(sgf_node), 68 | _ => Err(SgfParseError::UnexpectedGameType), 69 | } 70 | } 71 | 72 | /// Returns the [`GameType`] for this [`GameTree`]. 73 | /// 74 | /// # Examples 75 | /// ``` 76 | /// use sgf_parse::{parse, GameType}; 77 | /// 78 | /// let gametree = parse("(;GM[1]B[de]C[A comment])").unwrap().into_iter().next().unwrap(); 79 | /// assert_eq!(gametree.gametype(), GameType::Go); 80 | /// ``` 81 | pub fn gametype(&self) -> GameType { 82 | match self { 83 | Self::GoGame(_) => GameType::Go, 84 | Self::Unknown(_) => GameType::Unknown, 85 | } 86 | } 87 | } 88 | 89 | impl std::fmt::Display for GameTree { 90 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 91 | let node_text = match self { 92 | Self::GoGame(sgf_node) => sgf_node.serialize(), 93 | Self::Unknown(sgf_node) => sgf_node.serialize(), 94 | }; 95 | std::fmt::Display::fmt(&node_text, f) 96 | } 97 | } 98 | 99 | impl std::convert::From> for GameTree { 100 | fn from(sgf_node: SgfNode) -> Self { 101 | Self::GoGame(sgf_node) 102 | } 103 | } 104 | 105 | impl std::convert::From> for GameTree { 106 | fn from(sgf_node: SgfNode) -> Self { 107 | Self::Unknown(sgf_node) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /resources/test/ff4_ex.sgf: -------------------------------------------------------------------------------- 1 | (;FF[4]AP[Primiview:3.1]GM[1]SZ[19]GN[Gametree 1: properties]US[Arno Hollosi] 2 | 3 | (;B[pd]N[Moves, comments, annotations] 4 | C[Nodename set to: "Moves, comments, annotations"];W[dp]GW[1] 5 | C[Marked as "Good for White"];B[pp]GB[2] 6 | C[Marked as "Very good for Black"];W[dc]GW[2] 7 | C[Marked as "Very good for White"];B[pj]DM[1] 8 | C[Marked as "Even position"];W[ci]UC[1] 9 | C[Marked as "Unclear position"];B[jd]TE[1] 10 | C[Marked as "Tesuji" or "Good move"];W[jp]BM[2] 11 | C[Marked as "Very bad move"];B[gd]DO[] 12 | C[Marked as "Doubtful move"];W[de]IT[] 13 | C[Marked as "Interesting move"];B[jj]; 14 | C[White "Pass" move]W[]; 15 | C[Black "Pass" move]B[tt]) 16 | 17 | (;AB[dd][de][df][dg][do:gq] 18 | AW[jd][je][jf][jg][kn:lq][pn:pq] 19 | N[Setup]C[Black & white stones at the top are added as single stones. 20 | 21 | Black & white stones at the bottom are added using compressed point lists.] 22 | ;AE[ep][fp][kn][lo][lq][pn:pq] 23 | C[AddEmpty 24 | 25 | Black stones & stones of left white group are erased in FF[3\] way. 26 | 27 | White stones at bottom right were erased using compressed point list.] 28 | ;AB[pd]AW[pp]PL[B]C[Added two stones. 29 | 30 | Node marked with "Black to play".];PL[W] 31 | C[Node marked with "White to play"]) 32 | 33 | (;AB[dd][de][df][dg][dh][di][dj][nj][ni][nh][nf][ne][nd][ij][ii][ih][hq] 34 | [gq][fq][eq][dr][ds][dq][dp][cp][bp][ap][iq][ir][is][bo][bn][an][ms][mr] 35 | AW[pd][pe][pf][pg][ph][pi][pj][fd][fe][ff][fh][fi][fj][kh][ki][kj][os][or] 36 | [oq][op][pp][qp][rp][sp][ro][rn][sn][nq][mq][lq][kq][kr][ks][fs][gs][gr] 37 | [er]N[Markup]C[Position set up without compressed point lists.] 38 | 39 | ;TR[dd][de][df][ed][ee][ef][fd:ff] 40 | MA[dh][di][dj][ej][ei][eh][fh:fj] 41 | CR[nd][ne][nf][od][oe][of][pd:pf] 42 | SQ[nh][ni][nj][oh][oi][oj][ph:pj] 43 | SL[ih][ii][ij][jj][ji][jh][kh:kj] 44 | TW[pq:ss][so][lr:ns] 45 | TB[aq:cs][er:hs][ao] 46 | C[Markup at top partially using compressed point lists (for markup on white stones); listed clockwise, starting at upper left: 47 | - TR (triangle) 48 | - CR (circle) 49 | - SQ (square) 50 | - SL (selected points) 51 | - MA ('X') 52 | 53 | Markup at bottom: black & white territory (using compressed point lists)] 54 | ;LB[dc:1][fc:2][nc:3][pc:4][dj:a][fj:b][nj:c] 55 | [pj:d][gs:ABCDEFGH][gr:ABCDEFG][gq:ABCDEF][gp:ABCDE][go:ABCD][gn:ABC][gm:AB] 56 | [mm:12][mn:123][mo:1234][mp:12345][mq:123456][mr:1234567][ms:12345678] 57 | C[Label (LB property) 58 | 59 | Top: 8 single char labels (1-4, a-d) 60 | 61 | Bottom: Labels up to 8 char length.] 62 | 63 | ;DD[kq:os][dq:hs] 64 | AR[aa:sc][sa:ac][aa:sa][aa:ac][cd:cj] 65 | [gd:md][fh:ij][kj:nh] 66 | LN[pj:pd][nf:ff][ih:fj][kh:nj] 67 | C[Arrows, lines and dimmed points.]) 68 | 69 | (;B[qd]N[Style & text type] 70 | C[There are hard linebreaks & soft linebreaks. 71 | Soft linebreaks are linebreaks preceeded by '\\' like this one >o\ 72 | k<. Hard line breaks are all other linebreaks. 73 | Soft linebreaks are converted to >nothing<, i.e. removed. 74 | 75 | Note that linebreaks are coded differently on different systems. 76 | 77 | Examples (>ok< shouldn't be split): 78 | 79 | linebreak 1 "\\n": >o\ 80 | k< 81 | linebreak 2 "\\n\\r": >o\ 82 | k< 83 | linebreak 3 "\\r\\n": >o\ 84 | k< 85 | linebreak 4 "\\r": >o\ k<] 86 | 87 | (;W[dd]N[W d16]C[Variation C is better.](;B[pp]N[B q4]) 88 | (;B[dp]N[B d4]) 89 | (;B[pq]N[B q3]) 90 | (;B[oq]N[B p3]) 91 | ) 92 | (;W[dp]N[W d4]) 93 | (;W[pp]N[W q4]) 94 | (;W[cc]N[W c17]) 95 | (;W[cq]N[W c3]) 96 | (;W[qq]N[W r3]) 97 | ) 98 | 99 | (;B[qr]N[Time limits, captures & move numbers] 100 | BL[120.0]C[Black time left: 120 sec];W[rr] 101 | WL[300]C[White time left: 300 sec];B[rq] 102 | BL[105.6]OB[10]C[Black time left: 105.6 sec 103 | Black stones left (in this byo-yomi period): 10];W[qq] 104 | WL[200]OW[2]C[White time left: 200 sec 105 | White stones left: 2];B[sr] 106 | BL[87.00]OB[9]C[Black time left: 87 sec 107 | Black stones left: 9];W[qs] 108 | WL[13.20]OW[1]C[White time left: 13.2 sec 109 | White stones left: 1];B[rs] 110 | C[One white stone at s2 captured];W[ps];B[pr];W[or] 111 | MN[2]C[Set move number to 2];B[os] 112 | C[Two white stones captured 113 | (at q1 & r1)] 114 | ;MN[112]W[pq]C[Set move number to 112];B[sq];W[rp];B[ps] 115 | ;W[ns];B[ss];W[nr] 116 | ;B[rr];W[sp];B[qs]C[Suicide move 117 | (all B stones get captured)]) 118 | ) 119 | 120 | (;FF[4]AP[Primiview:3.1]GM[1]SZ[19]C[Gametree 2: game-info 121 | 122 | Game-info properties are usually stored in the root node. 123 | If games are merged into a single game-tree, they are stored in the node\ 124 | where the game first becomes distinguishable from all other games in\ 125 | the tree.] 126 | ;B[pd] 127 | (;PW[W. Hite]WR[6d]RO[2]RE[W+3.5] 128 | PB[B. Lack]BR[5d]PC[London]EV[Go Congress]W[dp] 129 | C[Game-info: 130 | Black: B. Lack, 5d 131 | White: W. Hite, 6d 132 | Place: London 133 | Event: Go Congress 134 | Round: 2 135 | Result: White wins by 3.5]) 136 | (;PW[T. Suji]WR[7d]RO[1]RE[W+Resign] 137 | PB[B. Lack]BR[5d]PC[London]EV[Go Congress]W[cp] 138 | C[Game-info: 139 | Black: B. Lack, 5d 140 | White: T. Suji, 7d 141 | Place: London 142 | Event: Go Congress 143 | Round: 1 144 | Result: White wins by resignation]) 145 | (;W[ep];B[pp] 146 | (;PW[S. Abaki]WR[1d]RO[3]RE[B+63.5] 147 | PB[B. Lack]BR[5d]PC[London]EV[Go Congress]W[ed] 148 | C[Game-info: 149 | Black: B. Lack, 5d 150 | White: S. Abaki, 1d 151 | Place: London 152 | Event: Go Congress 153 | Round: 3 154 | Result: Balck wins by 63.5]) 155 | (;PW[A. Tari]WR[12k]KM[-59.5]RO[4]RE[B+R] 156 | PB[B. Lack]BR[5d]PC[London]EV[Go Congress]W[cd] 157 | C[Game-info: 158 | Black: B. Lack, 5d 159 | White: A. Tari, 12k 160 | Place: London 161 | Event: Go Congress 162 | Round: 4 163 | Komi: -59.5 points 164 | Result: Black wins by resignation]) 165 | )) 166 | -------------------------------------------------------------------------------- /src/props/values.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use super::SgfPropError; 4 | 5 | /// An SGF [Color](https://www.red-bean.com/sgf/sgf4.html#types) value. 6 | #[derive(Copy, Clone, Debug, Eq, PartialEq)] 7 | pub enum Color { 8 | Black, 9 | White, 10 | } 11 | 12 | /// An SGF [Double](https://www.red-bean.com/sgf/sgf4.html#double) value. 13 | #[derive(Copy, Clone, Debug, Eq, PartialEq)] 14 | pub enum Double { 15 | One, 16 | Two, 17 | } 18 | 19 | /// An SGF [SimpleText](https://www.red-bean.com/sgf/sgf4.html#types) value. 20 | /// 21 | /// The text itself will be the raw text as stored in an sgf file. Displays formatted and escaped 22 | /// as [here](https://www.red-bean.com/sgf/sgf4.html#simpletext). 23 | /// 24 | /// # Examples 25 | /// ``` 26 | /// use sgf_parse::SimpleText; 27 | /// 28 | /// let text = SimpleText { text: "Comment:\nall whitespace\treplaced".to_string() }; 29 | /// assert_eq!(format!("{}", text), "Comment: all whitespace replaced"); 30 | /// ``` 31 | #[derive(Clone, Debug, Eq, PartialEq, Hash)] 32 | pub struct SimpleText { 33 | pub text: String, 34 | } 35 | 36 | /// An SGF [Text](https://www.red-bean.com/sgf/sgf4.html#types) value. 37 | /// 38 | /// The text itself will be the raw text as stored in an sgf file. Displays formatted and escaped 39 | /// as [here](https://www.red-bean.com/sgf/sgf4.html#text). 40 | /// 41 | /// # Examples 42 | /// ``` 43 | /// use sgf_parse::Text; 44 | /// let text = Text { text: "Comment:\nnon-linebreak whitespace\treplaced".to_string() }; 45 | /// assert_eq!(format!("{}", text), "Comment:\nnon-linebreak whitespace replaced"); 46 | /// ``` 47 | #[derive(Clone, Debug, Eq, PartialEq, Hash)] 48 | pub struct Text { 49 | pub text: String, 50 | } 51 | 52 | /// An SGF [property type](https://www.red-bean.com/sgf/sgf4.html#2.2.1). 53 | #[derive(Copy, Clone, Debug, Eq, PartialEq)] 54 | pub enum PropertyType { 55 | Move, 56 | Setup, 57 | Root, 58 | GameInfo, 59 | Inherit, 60 | } 61 | 62 | impl FromStr for Double { 63 | type Err = SgfPropError; 64 | 65 | fn from_str(s: &str) -> Result { 66 | if s == "1" { 67 | Ok(Self::One) 68 | } else if s == "2" { 69 | Ok(Self::Two) 70 | } else { 71 | Err(SgfPropError {}) 72 | } 73 | } 74 | } 75 | 76 | impl FromStr for Color { 77 | type Err = SgfPropError; 78 | 79 | fn from_str(s: &str) -> Result { 80 | if s == "B" { 81 | Ok(Self::Black) 82 | } else if s == "W" { 83 | Ok(Self::White) 84 | } else { 85 | Err(SgfPropError {}) 86 | } 87 | } 88 | } 89 | 90 | impl std::convert::From<&str> for SimpleText { 91 | fn from(s: &str) -> Self { 92 | Self { text: s.to_owned() } 93 | } 94 | } 95 | 96 | impl std::convert::From<&str> for Text { 97 | fn from(s: &str) -> Self { 98 | Self { text: s.to_owned() } 99 | } 100 | } 101 | 102 | impl FromStr for SimpleText { 103 | type Err = SgfPropError; 104 | 105 | fn from_str(s: &str) -> Result { 106 | Ok(s.into()) 107 | } 108 | } 109 | 110 | impl FromStr for Text { 111 | type Err = SgfPropError; 112 | 113 | fn from_str(s: &str) -> Result { 114 | Ok(s.into()) 115 | } 116 | } 117 | 118 | impl std::fmt::Display for SimpleText { 119 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 120 | let text = format_text(&self.text) 121 | .replace("\r\n", " ") 122 | .replace("\n\r", " ") 123 | .replace(['\n', '\r'], " "); 124 | f.write_str(&text) 125 | } 126 | } 127 | 128 | impl std::fmt::Display for Text { 129 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 130 | let text = format_text(&self.text); 131 | f.write_str(&text) 132 | } 133 | } 134 | 135 | fn format_text(s: &str) -> String { 136 | // See https://www.red-bean.com/sgf/sgf4.html#text 137 | let mut output = vec![]; 138 | let chars: Vec = s.chars().collect(); 139 | let mut i = 0; 140 | while i < chars.len() { 141 | let c = chars[i]; 142 | if c == '\\' && i + 1 < chars.len() { 143 | i += 1; 144 | 145 | // Remove soft line breaks 146 | if chars[i] == '\n' { 147 | if i + 1 < chars.len() && chars[i + 1] == '\r' { 148 | i += 1; 149 | } 150 | } else if chars[i] == '\r' { 151 | if i + 1 < chars.len() && chars[i + 1] == '\n' { 152 | i += 1; 153 | } 154 | } else { 155 | // Push any other literal char following '\' 156 | output.push(chars[i]); 157 | } 158 | } else if c.is_whitespace() && c != '\r' && c != '\n' { 159 | if i + 1 < chars.len() { 160 | let next = chars[i + 1]; 161 | // Treat \r\n or \n\r as a single linebreak 162 | if (c == '\n' && next == '\r') || (c == '\r' && next == '\n') { 163 | i += 1; 164 | } 165 | } 166 | // Replace whitespace with ' ' 167 | output.push(' '); 168 | } else { 169 | output.push(c); 170 | } 171 | i += 1; 172 | } 173 | 174 | output.into_iter().collect() 175 | } 176 | 177 | #[cfg(test)] 178 | mod test { 179 | #[test] 180 | pub fn format_text() { 181 | let text = super::Text { 182 | text: "Comment with\trandom whitespace\nescaped \\] and \\\\ and a soft \\\nlinebreak" 183 | .to_string(), 184 | }; 185 | let expected = "Comment with random whitespace\nescaped ] and \\ and a soft linebreak"; 186 | 187 | assert_eq!(format!("{}", text), expected); 188 | } 189 | 190 | #[test] 191 | pub fn format_simple_text() { 192 | let text = super::SimpleText { text: 193 | "Comment with\trandom\r\nwhitespace\n\rescaped \\] and \\\\ and\na soft \\\nlinebreak" 194 | .to_string() 195 | }; 196 | let expected = "Comment with random whitespace escaped ] and \\ and a soft linebreak"; 197 | 198 | assert_eq!(format!("{}", text), expected); 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/lexer.rs: -------------------------------------------------------------------------------- 1 | pub fn tokenize( 2 | text: &str, 3 | ) -> impl Iterator), LexerError>> + '_ { 4 | Lexer { text, cursor: 0 } 5 | } 6 | 7 | #[derive(Debug, PartialEq)] 8 | pub enum Token { 9 | StartGameTree, 10 | EndGameTree, 11 | StartNode, 12 | Property((String, Vec)), 13 | } 14 | 15 | /// Error type for failures to tokenize text. 16 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 17 | pub enum LexerError { 18 | UnexpectedPropertyIdentifier, 19 | MissingPropertyIdentifier, 20 | UnexpectedEndOfPropertyIdentifier, 21 | UnexpectedEndOfPropertyValue, 22 | } 23 | 24 | impl std::fmt::Display for LexerError { 25 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 26 | match self { 27 | LexerError::UnexpectedPropertyIdentifier => { 28 | write!(f, "Unexpected property identifier value") 29 | } 30 | LexerError::MissingPropertyIdentifier => { 31 | write!(f, "Missing property identifier") 32 | } 33 | LexerError::UnexpectedEndOfPropertyIdentifier => { 34 | write!(f, "Unexpected end of property identifier") 35 | } 36 | LexerError::UnexpectedEndOfPropertyValue => { 37 | write!(f, "Unexpected end of property value") 38 | } 39 | } 40 | } 41 | } 42 | 43 | impl std::error::Error for LexerError {} 44 | 45 | struct Lexer<'a> { 46 | text: &'a str, 47 | cursor: usize, 48 | } 49 | 50 | impl Lexer<'_> { 51 | fn trim_leading_whitespace(&mut self) { 52 | while self.cursor < self.text.len() 53 | && (self.text.as_bytes()[self.cursor] as char).is_ascii_whitespace() 54 | { 55 | self.cursor += 1; 56 | } 57 | } 58 | 59 | fn get_char(&mut self) -> Option { 60 | let result = self.text[self.cursor..].chars().next(); 61 | result.iter().for_each(|c| self.cursor += c.len_utf8()); 62 | 63 | result 64 | } 65 | 66 | fn peek_char(&self) -> Option { 67 | self.text[self.cursor..].chars().next() 68 | } 69 | 70 | fn get_property(&mut self) -> Result<(String, Vec), LexerError> { 71 | Ok((self.get_prop_ident()?, self.get_prop_values()?)) 72 | } 73 | 74 | fn get_prop_ident(&mut self) -> Result { 75 | let mut prop_ident = vec![]; 76 | loop { 77 | match self.peek_char() { 78 | Some('[') => break, 79 | Some(c) if c.is_ascii() => { 80 | self.cursor += 1; 81 | prop_ident.push(c); 82 | } 83 | Some(_c) => return Err(LexerError::UnexpectedEndOfPropertyIdentifier), 84 | None => return Err(LexerError::MissingPropertyIdentifier), 85 | } 86 | } 87 | 88 | Ok(prop_ident.iter().collect()) 89 | } 90 | 91 | fn get_prop_values(&mut self) -> Result, LexerError> { 92 | let mut prop_values = vec![]; 93 | loop { 94 | self.trim_leading_whitespace(); 95 | match self.peek_char() { 96 | Some('[') => { 97 | self.cursor += 1; 98 | prop_values.push(self.get_prop_value()?); 99 | } 100 | _ => break, 101 | } 102 | } 103 | 104 | Ok(prop_values) 105 | } 106 | 107 | fn get_prop_value(&mut self) -> Result { 108 | let mut prop_value = vec![]; 109 | let mut escaped = false; 110 | loop { 111 | match self.get_char() { 112 | Some(']') if !escaped => break, 113 | Some('\\') if !escaped => escaped = true, 114 | Some(c) => { 115 | escaped = false; 116 | prop_value.push(c); 117 | } 118 | None => return Err(LexerError::UnexpectedEndOfPropertyValue), 119 | } 120 | } 121 | 122 | Ok(prop_value.iter().collect()) 123 | } 124 | } 125 | 126 | impl Iterator for Lexer<'_> { 127 | type Item = Result<(Token, std::ops::Range), LexerError>; 128 | 129 | fn next(&mut self) -> Option { 130 | let span_start = self.cursor; 131 | let token = match self.peek_char() { 132 | Some('(') => { 133 | self.cursor += 1; 134 | Token::StartGameTree 135 | } 136 | Some(')') => { 137 | self.cursor += 1; 138 | Token::EndGameTree 139 | } 140 | Some(';') => { 141 | self.cursor += 1; 142 | Token::StartNode 143 | } 144 | None => return None, 145 | _ => match self.get_property() { 146 | Ok(property) => Token::Property(property), 147 | Err(e) => return Some(Err(e)), 148 | }, 149 | }; 150 | let span = span_start..self.cursor; 151 | self.trim_leading_whitespace(); 152 | 153 | Some(Ok((token, span))) 154 | } 155 | } 156 | 157 | #[cfg(test)] 158 | mod test { 159 | use super::tokenize; 160 | use super::Token::*; 161 | 162 | #[test] 163 | fn lexer() { 164 | let sgf = "(;SZ[9]C[Some comment];B[de];W[fe])(;B[de];W[ff])"; 165 | let expected = vec![ 166 | (StartGameTree, 0..1), 167 | (StartNode, 1..2), 168 | (Property(("SZ".to_string(), vec!["9".to_string()])), 2..7), 169 | ( 170 | Property(("C".to_string(), vec!["Some comment".to_string()])), 171 | 7..22, 172 | ), 173 | (StartNode, 22..23), 174 | (Property(("B".to_string(), vec!["de".to_string()])), 23..28), 175 | (StartNode, 28..29), 176 | (Property(("W".to_string(), vec!["fe".to_string()])), 29..34), 177 | (EndGameTree, 34..35), 178 | (StartGameTree, 35..36), 179 | (StartNode, 36..37), 180 | (Property(("B".to_string(), vec!["de".to_string()])), 37..42), 181 | (StartNode, 42..43), 182 | (Property(("W".to_string(), vec!["ff".to_string()])), 43..48), 183 | (EndGameTree, 48..49), 184 | ]; 185 | let tokens: Vec<_> = tokenize(sgf).collect::>().unwrap(); 186 | 187 | assert_eq!(tokens, expected); 188 | } 189 | 190 | #[test] 191 | fn handles_old_style_properties() { 192 | let sgf = "(;CoPyright[text])"; 193 | let expected = vec![ 194 | (StartGameTree, 0..1), 195 | (StartNode, 1..2), 196 | ( 197 | Property(("CoPyright".to_string(), vec!["text".to_string()])), 198 | 2..17, 199 | ), 200 | (EndGameTree, 17..18), 201 | ]; 202 | let tokens: Vec<_> = tokenize(sgf).collect::>().unwrap(); 203 | 204 | assert_eq!(tokens, expected); 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/go.rs: -------------------------------------------------------------------------------- 1 | //! Types specific to the game of Go. 2 | //! 3 | //! This module contains a go-specific [`SgfProp`] implementation which 4 | //! includes go specific properties (HA, KM, TB, TW). Point and Stone values 5 | //! map to [`Point`], and Move values map to [`Move`]. Properties with 6 | //! invalid moves or points map to [`Prop::Invalid`] (as do any invalid 7 | //! [general properties](https://www.red-bean.com/sgf/properties.html)). 8 | //! 9 | //! This module also includes a convenience [`parse`] function which fails 10 | //! on non-go games and returns the [`SgfNode`] values directly instead of 11 | //! returning [`GameTree`](crate::GameTree) values. 12 | use std::collections::HashSet; 13 | 14 | use crate::props::parse::{parse_elist, parse_single_value, FromCompressedList}; 15 | use crate::props::{PropertyType, SgfPropError, ToSgf}; 16 | use crate::{InvalidNodeError, SgfNode, SgfParseError, SgfProp}; 17 | 18 | /// Returns the [`SgfNode`] values for Go games parsed from the provided text. 19 | /// 20 | /// This is a convenience wrapper around [`crate::parse`] for dealing with Go only collections. 21 | /// 22 | /// # Errors 23 | /// If the text can't be parsed as an SGF FF\[4\] collection, then an error is returned. 24 | /// 25 | /// # Examples 26 | /// ``` 27 | /// use sgf_parse::go::parse; 28 | /// 29 | /// // Prints the all the properties for the two root nodes in the SGF 30 | /// let sgf = "(;SZ[9]C[Some comment];B[de];W[fe])(;B[de];W[ff])"; 31 | /// for node in parse(&sgf).unwrap().iter() { 32 | /// for prop in node.properties() { 33 | /// println!("{:?}", prop); 34 | /// } 35 | /// } 36 | /// ``` 37 | pub fn parse(text: &str) -> Result>, SgfParseError> { 38 | let gametrees = crate::parse(text)?; 39 | gametrees 40 | .into_iter() 41 | .map(|gametree| gametree.into_go_node()) 42 | .collect::, _>>() 43 | } 44 | 45 | /// An SGF [Point](https://www.red-bean.com/sgf/go.html#types) value for the Game of Go. 46 | /// 47 | /// # Examples 48 | /// ``` 49 | /// use sgf_parse::go::{Prop, Move, Point}; 50 | /// 51 | /// let point = Point {x: 10, y: 10}; 52 | /// let prop = Prop::B(Move::Move(point)); 53 | /// ``` 54 | #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] 55 | pub struct Point { 56 | pub x: u8, 57 | pub y: u8, 58 | } 59 | 60 | /// An SGF [Stone](https://www.red-bean.com/sgf/go.html#types) value for the Game of Go. 61 | pub type Stone = Point; 62 | 63 | /// An SGF [Move](https://www.red-bean.com/sgf/go.html#types) value for the Game of Go. 64 | /// 65 | /// # Examples 66 | /// ``` 67 | /// use sgf_parse::go::{parse, Move, Prop}; 68 | /// 69 | /// let node = parse("(;B[de])").unwrap().into_iter().next().unwrap(); 70 | /// for prop in node.properties() { 71 | /// match prop { 72 | /// Prop::B(Move::Move(point)) => println!("B move at {:?}", point), 73 | /// _ => {} 74 | /// } 75 | /// } 76 | /// ``` 77 | #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] 78 | pub enum Move { 79 | Pass, 80 | Move(Point), 81 | } 82 | 83 | sgf_prop! { 84 | Prop, Move, Point, Point, 85 | { 86 | HA(i64), 87 | KM(f64), 88 | TB(HashSet), 89 | TW(HashSet), 90 | } 91 | } 92 | 93 | impl SgfProp for Prop { 94 | type Point = Point; 95 | type Stone = Stone; 96 | type Move = Move; 97 | 98 | fn new(identifier: String, values: Vec) -> Self { 99 | match Prop::parse_general_prop(identifier, values) { 100 | Self::Unknown(identifier, values) => match &identifier[..] { 101 | "KM" => parse_single_value(&values) 102 | .map_or_else(|_| Self::Invalid(identifier, values), Self::KM), 103 | 104 | "HA" => match parse_single_value(&values) { 105 | Ok(value) => { 106 | if value < 2 { 107 | Self::Invalid(identifier, values) 108 | } else { 109 | Self::HA(value) 110 | } 111 | } 112 | _ => Self::Invalid(identifier, values), 113 | }, 114 | "TB" => parse_elist(&values) 115 | .map_or_else(|_| Self::Invalid(identifier, values), Self::TB), 116 | "TW" => parse_elist(&values) 117 | .map_or_else(|_| Self::Invalid(identifier, values), Self::TW), 118 | _ => Self::Unknown(identifier, values), 119 | }, 120 | prop => prop, 121 | } 122 | } 123 | 124 | fn identifier(&self) -> String { 125 | match self.general_identifier() { 126 | Some(identifier) => identifier, 127 | None => match self { 128 | Self::KM(_) => "KM".to_string(), 129 | Self::HA(_) => "HA".to_string(), 130 | Self::TB(_) => "TB".to_string(), 131 | Self::TW(_) => "TW".to_string(), 132 | _ => panic!("Unimplemented identifier for {:?}", self), 133 | }, 134 | } 135 | } 136 | 137 | fn property_type(&self) -> Option { 138 | match self.general_property_type() { 139 | Some(property_type) => Some(property_type), 140 | None => match self { 141 | Self::HA(_) => Some(PropertyType::GameInfo), 142 | Self::KM(_) => Some(PropertyType::GameInfo), 143 | _ => None, 144 | }, 145 | } 146 | } 147 | 148 | fn validate_properties(properties: &[Self], is_root: bool) -> Result<(), InvalidNodeError> { 149 | Self::general_validate_properties(properties, is_root) 150 | } 151 | } 152 | 153 | impl std::fmt::Display for Prop { 154 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 155 | let prop_string = match self.serialize_prop_value() { 156 | Some(s) => s, 157 | None => match self { 158 | Self::HA(x) => x.to_sgf(), 159 | Self::KM(x) => x.to_sgf(), 160 | Self::TB(x) => x.to_sgf(), 161 | Self::TW(x) => x.to_sgf(), 162 | _ => panic!("Unimplemented identifier for {:?}", self), 163 | }, 164 | }; 165 | write!(f, "{}[{}]", self.identifier(), prop_string) 166 | } 167 | } 168 | 169 | impl FromCompressedList for Point { 170 | fn from_compressed_list(ul: &Self, lr: &Self) -> Result, SgfPropError> { 171 | let mut points = HashSet::new(); 172 | if ul.x > lr.x || ul.y > lr.y { 173 | return Err(SgfPropError {}); 174 | } 175 | for x in ul.x..=lr.x { 176 | for y in ul.y..=lr.y { 177 | let point = Self { x, y }; 178 | if points.contains(&point) { 179 | return Err(SgfPropError {}); 180 | } 181 | points.insert(point); 182 | } 183 | } 184 | Ok(points) 185 | } 186 | } 187 | 188 | impl ToSgf for Move { 189 | fn to_sgf(&self) -> String { 190 | match self { 191 | Self::Pass => "".to_string(), 192 | Self::Move(point) => point.to_sgf(), 193 | } 194 | } 195 | } 196 | 197 | impl ToSgf for Point { 198 | fn to_sgf(&self) -> String { 199 | format!("{}{}", (self.x + b'a') as char, (self.y + b'a') as char) 200 | } 201 | } 202 | 203 | impl std::str::FromStr for Move { 204 | type Err = SgfPropError; 205 | 206 | fn from_str(s: &str) -> Result { 207 | match s { 208 | "" => Ok(Self::Pass), 209 | _ => Ok(Self::Move(s.parse()?)), 210 | } 211 | } 212 | } 213 | 214 | impl std::str::FromStr for Point { 215 | type Err = SgfPropError; 216 | 217 | fn from_str(s: &str) -> Result { 218 | fn map_char(c: char) -> Result { 219 | if c.is_ascii_lowercase() { 220 | Ok(c as u8 - b'a') 221 | } else if c.is_ascii_uppercase() { 222 | Ok(c as u8 - b'A' + 26) 223 | } else { 224 | Err(SgfPropError {}) 225 | } 226 | } 227 | 228 | let chars: Vec = s.chars().collect(); 229 | if chars.len() != 2 { 230 | return Err(SgfPropError {}); 231 | } 232 | 233 | Ok(Self { 234 | x: map_char(chars[0])?, 235 | y: map_char(chars[1])?, 236 | }) 237 | } 238 | } 239 | 240 | #[cfg(test)] 241 | mod tests { 242 | use super::Point; 243 | 244 | #[test] 245 | fn large_move_numbers() { 246 | let point: Point = "aC".parse().unwrap(); 247 | let expected = Point { x: 0, y: 28 }; 248 | assert_eq!(point, expected); 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /src/sgf_node.rs: -------------------------------------------------------------------------------- 1 | use crate::props::{PropertyType, SgfProp}; 2 | 3 | /// A node in an SGF Game Tree. 4 | /// 5 | /// Any succesfully constructed node will be serializable, but may or may not be valid. 6 | /// All game-specific information is encoded in the `Prop` type. Use 7 | /// [`go::Prop`](`crate::go::Prop`) for go games, and 8 | /// [`unknown_game::Prop`](`crate::unknown_game::Prop`) for all other games. 9 | #[derive(Clone, Debug, PartialEq)] 10 | pub struct SgfNode { 11 | pub properties: Vec, 12 | pub children: Vec, 13 | pub is_root: bool, 14 | } 15 | 16 | impl Default for SgfNode { 17 | fn default() -> Self { 18 | Self { 19 | properties: vec![], 20 | children: vec![], 21 | is_root: false, 22 | } 23 | } 24 | } 25 | 26 | impl SgfNode { 27 | /// Returns a new node. 28 | /// 29 | /// # Examples 30 | /// ``` 31 | /// use sgf_parse::{SgfNode, SgfProp}; 32 | /// use sgf_parse::go::Prop; 33 | /// 34 | /// let children = vec![ 35 | /// SgfNode::::new( 36 | /// vec![Prop::new("B".to_string(), vec!["dd".to_string()])], 37 | /// vec![], 38 | /// false, 39 | /// ), 40 | /// ]; 41 | /// let node = SgfNode::new(vec![Prop::SZ((19, 19))], children, true); 42 | /// ``` 43 | pub fn new(properties: Vec, children: Vec, is_root: bool) -> Self { 44 | Self { 45 | properties, 46 | children, 47 | is_root, 48 | } 49 | } 50 | 51 | /// Returns the property with the provided identifier for the node (if present). 52 | /// 53 | /// # Examples 54 | /// ``` 55 | /// use sgf_parse::go::{parse, Prop}; 56 | /// 57 | /// let node = parse("(;SZ[13:13];B[de])").unwrap().into_iter().next().unwrap(); 58 | /// let board_size = match node.get_property("SZ") { 59 | /// Some(Prop::SZ(size)) => size.clone(), 60 | /// None => (19, 19), 61 | /// _ => unreachable!(), 62 | /// }; 63 | /// ``` 64 | pub fn get_property(&self, identifier: &str) -> Option<&Prop> { 65 | self.properties 66 | .iter() 67 | .find(|&prop| prop.identifier() == identifier) 68 | } 69 | 70 | /// Returns an iterator over the children of this node. 71 | /// 72 | /// # Examples 73 | /// ``` 74 | /// use sgf_parse::go::parse; 75 | /// 76 | /// let node = parse("(;SZ[19](;B[de])(;B[dd]HO[2]))").unwrap().into_iter().next().unwrap(); 77 | /// for child in node.children() { 78 | /// if let Some(prop) = child.get_property("HO") { 79 | /// println!("Found a hotspot!") 80 | /// } 81 | /// } 82 | /// ``` 83 | pub fn children(&self) -> impl Iterator { 84 | self.children.iter() 85 | } 86 | 87 | /// Returns an iterator over the properties of this node. 88 | /// 89 | /// # Examples 90 | /// ``` 91 | /// use sgf_parse::go::{parse, Move, Prop}; 92 | /// 93 | /// let node = parse("(;B[de]C[A comment])").unwrap().into_iter().next().unwrap(); 94 | /// for prop in node.properties() { 95 | /// match prop { 96 | /// Prop::B(mv) => match mv { 97 | /// Move::Move(p) => println!("B Move at {}, {}", p.x, p.y), 98 | /// Move::Pass => println!("B Pass"), 99 | /// } 100 | /// Prop::W(mv) => match mv { 101 | /// Move::Move(p) => println!("W Move at {}, {}", p.x, p.y), 102 | /// Move::Pass => println!("W Pass"), 103 | /// } 104 | /// _ => {}, 105 | /// } 106 | /// } 107 | /// ``` 108 | pub fn properties(&self) -> impl Iterator { 109 | self.properties.iter() 110 | } 111 | 112 | /// Returns the serialized SGF for this SgfNode as a complete GameTree. 113 | /// 114 | /// # Examples 115 | /// ``` 116 | /// use sgf_parse::go::parse; 117 | /// 118 | /// let sgf = "(;SZ[13:13];B[de])"; 119 | /// let node = parse(sgf).unwrap().into_iter().next().unwrap(); 120 | /// assert_eq!(node.serialize(), sgf); 121 | /// ``` 122 | pub fn serialize(&self) -> String { 123 | format!("({self})") 124 | } 125 | 126 | /// Returns `Ok` if the node's properties are valid according to the SGF FF\[4\] spec. 127 | /// 128 | /// # Errors 129 | /// Returns an error if the node has invalid properties. 130 | /// 131 | /// # Examples 132 | /// ``` 133 | /// use sgf_parse::InvalidNodeError; 134 | /// use sgf_parse::go::parse; 135 | /// 136 | /// let node = parse("(;B[de]C[A comment]C[Another])").unwrap().into_iter().next().unwrap(); 137 | /// let result = node.validate(); 138 | /// assert!(matches!(result, Err(InvalidNodeError::RepeatedIdentifier(_)))); 139 | /// ``` 140 | pub fn validate(&self) -> Result<(), InvalidNodeError> { 141 | // TODO: Implement this non-recursively 142 | self.validate_helper()?; 143 | Ok(()) 144 | } 145 | 146 | // Helper that returns whether a child has any game info in its descendents. 147 | fn validate_helper(&self) -> Result { 148 | Prop::validate_properties(&self.properties, self.is_root)?; 149 | let has_game_info = self.has_game_info(); 150 | let mut child_has_game_info = false; 151 | for child in self.children() { 152 | child_has_game_info |= child.validate_helper()?; 153 | } 154 | if child_has_game_info && has_game_info { 155 | return Err(InvalidNodeError::UnexpectedGameInfo(format!( 156 | "{:?}", 157 | self.properties 158 | ))); 159 | } 160 | Ok(has_game_info) 161 | } 162 | 163 | /// Returns an iterator over the nodes of the main variation. 164 | /// 165 | /// This is a convenience method for iterating through the first child of each node until the 166 | /// main line ends. 167 | /// 168 | /// # Examples 169 | /// ``` 170 | /// use crate::sgf_parse::SgfProp; 171 | /// use sgf_parse::go::{parse, Prop}; 172 | /// 173 | /// let sgf = "(;B[ee];W[ce](;B[ge](;W[gd])(;W[gf]))(;B[ce]))"; 174 | /// let node = &parse(sgf).unwrap()[0]; 175 | /// 176 | /// let moves: Vec = node 177 | /// .main_variation() 178 | /// .map(|n| { 179 | /// n.get_property("B") 180 | /// .or_else(|| n.get_property("W")) 181 | /// .unwrap() 182 | /// .clone() 183 | /// }) 184 | /// .collect(); 185 | /// let expected = vec![ 186 | /// Prop::new("B".to_string(), vec!["ee".to_string()]), 187 | /// Prop::new("W".to_string(), vec!["ce".to_string()]), 188 | /// Prop::new("B".to_string(), vec!["ge".to_string()]), 189 | /// Prop::new("W".to_string(), vec!["gd".to_string()]), 190 | /// ]; 191 | /// 192 | /// assert_eq!(moves, expected); 193 | /// ``` 194 | pub fn main_variation(&self) -> impl Iterator { 195 | MainVariationIter { 196 | node: Some(self), 197 | started: false, 198 | } 199 | } 200 | 201 | /// Returns the move property (if present) on the node. 202 | /// 203 | /// # Examples 204 | /// ``` 205 | /// use crate::sgf_parse::SgfProp; 206 | /// use sgf_parse::go::{parse, Prop}; 207 | /// let sgf = "(;GM[1]B[tt]C[Comment])"; 208 | /// let node = &parse(sgf).unwrap()[0]; 209 | /// 210 | /// let mv = node.get_move(); 211 | /// assert_eq!(mv, Some(&Prop::new("B".to_string(), vec!["tt".to_string()]))); 212 | /// ``` 213 | pub fn get_move(&self) -> Option<&Prop> { 214 | // Since there can only be one move per node in an sgf, this is safe. 215 | self.properties() 216 | .find(|p| p.property_type() == Some(PropertyType::Move)) 217 | } 218 | 219 | fn has_game_info(&self) -> bool { 220 | for prop in self.properties() { 221 | if let Some(PropertyType::GameInfo) = prop.property_type() { 222 | return true; 223 | } 224 | } 225 | false 226 | } 227 | } 228 | 229 | impl std::fmt::Display for SgfNode { 230 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 231 | // TODO: Implement this non-recursively 232 | let prop_string = self 233 | .properties() 234 | .map(|x| x.to_string()) 235 | .collect::>() 236 | .join(""); 237 | let child_count = self.children().count(); 238 | let child_string = match child_count { 239 | 0 => "".to_string(), 240 | 1 => self.children().next().unwrap().to_string(), 241 | _ => self 242 | .children() 243 | .map(|x| format!("({x})")) 244 | .collect::>() 245 | .join(""), 246 | }; 247 | write!(f, ";{prop_string}{child_string}") 248 | } 249 | } 250 | 251 | #[derive(Debug)] 252 | struct MainVariationIter<'a, Prop: SgfProp> { 253 | node: Option<&'a SgfNode>, 254 | started: bool, 255 | } 256 | 257 | impl<'a, Prop: SgfProp> Iterator for MainVariationIter<'a, Prop> { 258 | type Item = &'a SgfNode; 259 | 260 | fn next(&mut self) -> Option { 261 | if self.started { 262 | self.node = self.node.and_then(|n| n.children().next()); 263 | } else { 264 | self.started = true; 265 | } 266 | self.node 267 | } 268 | } 269 | 270 | /// Err type for [`SgfNode::validate`]. 271 | #[derive(Debug, Clone, PartialEq, Eq)] 272 | pub enum InvalidNodeError { 273 | UnexpectedRootProperties(String), 274 | UnexpectedGameInfo(String), 275 | RepeatedMarkup(String), 276 | MultipleMoves(String), 277 | RepeatedIdentifier(String), 278 | SetupAndMove(String), 279 | KoWithoutMove(String), 280 | MultipleMoveAnnotations(String), 281 | UnexpectedMoveAnnotation(String), 282 | MultipleExclusiveAnnotations(String), 283 | InvalidProperty(String), 284 | } 285 | 286 | impl std::fmt::Display for InvalidNodeError { 287 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 288 | match self { 289 | InvalidNodeError::UnexpectedRootProperties(context) => { 290 | write!(f, "Root properties in non-root node: {context:?}") 291 | } 292 | InvalidNodeError::UnexpectedGameInfo(context) => { 293 | write!(f, "GameInfo properties in node and a child {context:?}") 294 | } 295 | InvalidNodeError::RepeatedMarkup(context) => { 296 | write!(f, "Multiple markup properties on same point {context:?}") 297 | } 298 | InvalidNodeError::MultipleMoves(context) => { 299 | write!(f, "B and W moves in same node {context:?}") 300 | } 301 | InvalidNodeError::RepeatedIdentifier(context) => { 302 | write!(f, "Identifier repeated in node {context:?}") 303 | } 304 | InvalidNodeError::SetupAndMove(context) => { 305 | write!(f, "Setup and move properties in same node {context:?}") 306 | } 307 | InvalidNodeError::KoWithoutMove(context) => { 308 | write!(f, "Ko in node without B or W {context:?}") 309 | } 310 | InvalidNodeError::MultipleMoveAnnotations(context) => { 311 | write!(f, "Multiple move annotations in same node {context:?}") 312 | } 313 | InvalidNodeError::UnexpectedMoveAnnotation(context) => { 314 | write!(f, "Move annotation without move in node {context:?}") 315 | } 316 | InvalidNodeError::MultipleExclusiveAnnotations(context) => { 317 | write!( 318 | f, 319 | "Multiple DM, UC, GW or GB properties in node {context:?}" 320 | ) 321 | } 322 | InvalidNodeError::InvalidProperty(context) => { 323 | write!(f, "Invalid property: {context:?}") 324 | } 325 | } 326 | } 327 | } 328 | 329 | impl std::error::Error for InvalidNodeError {} 330 | 331 | #[cfg(test)] 332 | mod tests { 333 | use super::InvalidNodeError; 334 | use crate::go::parse; 335 | 336 | #[test] 337 | fn validate_sample_sgf_valid() { 338 | let mut sgf_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); 339 | sgf_path.push("resources/test/ff4_ex.sgf"); 340 | let sgf = std::fs::read_to_string(sgf_path).unwrap(); 341 | let node = &parse(&sgf).unwrap()[0]; 342 | assert!(node.validate().is_ok()); 343 | } 344 | 345 | #[test] 346 | fn validate_valid_node() { 347 | let sgf = "(;SZ[9]HA[3]C[Some comment];B[de];W[fe])"; 348 | let node = &parse(sgf).unwrap()[0]; 349 | assert!(node.validate().is_ok()); 350 | } 351 | 352 | #[test] 353 | fn validate_unexpected_root_properties() { 354 | let sgf = "(;SZ[9]C[Some comment];GM[1])"; 355 | let node = &parse(sgf).unwrap()[0]; 356 | assert!(matches!( 357 | node.validate(), 358 | Err(InvalidNodeError::UnexpectedRootProperties(_)) 359 | )); 360 | } 361 | 362 | #[test] 363 | fn validate_unexpected_game_info() { 364 | let sgf = "(;SZ[9]KM[3.5]C[Some comment];HA[3])"; 365 | let node = &parse(sgf).unwrap()[0]; 366 | assert!(matches!( 367 | node.validate(), 368 | Err(InvalidNodeError::UnexpectedGameInfo(_)) 369 | )); 370 | } 371 | 372 | #[test] 373 | fn validate_repeated_markup() { 374 | let sgf = "(;SZ[9]KM[3.5]C[Some comment];CR[dd]TR[dd])"; 375 | let node = &parse(sgf).unwrap()[0]; 376 | assert!(matches!( 377 | node.validate(), 378 | Err(InvalidNodeError::RepeatedMarkup(_)) 379 | )); 380 | } 381 | 382 | #[test] 383 | fn validate_multiple_moves() { 384 | let sgf = "(;SZ[9]C[Some comment];B[dd]W[cd])"; 385 | let node = &parse(sgf).unwrap()[0]; 386 | assert!(matches!( 387 | node.validate(), 388 | Err(InvalidNodeError::MultipleMoves(_)) 389 | )); 390 | } 391 | 392 | #[test] 393 | fn validate_repeated_identifier() { 394 | let sgf = "(;SZ[9]HA[3]HA[4])"; 395 | let node = &parse(sgf).unwrap()[0]; 396 | assert!(matches!( 397 | node.validate(), 398 | Err(InvalidNodeError::RepeatedIdentifier(_)) 399 | )); 400 | } 401 | 402 | #[test] 403 | fn validate_setup_and_move() { 404 | let sgf = "(;AB[dd]B[cc])"; 405 | let node = &parse(sgf).unwrap()[0]; 406 | assert!(matches!( 407 | node.validate(), 408 | Err(InvalidNodeError::SetupAndMove(_)) 409 | )); 410 | } 411 | 412 | #[test] 413 | fn validate_ko_without_move() { 414 | let sgf = "(;KO[])"; 415 | let node = &parse(sgf).unwrap()[0]; 416 | assert!(matches!( 417 | node.validate(), 418 | Err(InvalidNodeError::KoWithoutMove(_)) 419 | )); 420 | } 421 | 422 | #[test] 423 | fn validate_multiple_move_annotations() { 424 | let sgf = "(;B[dd]DO[]BM[1])"; 425 | let node = &parse(sgf).unwrap()[0]; 426 | assert!(matches!( 427 | node.validate(), 428 | Err(InvalidNodeError::MultipleMoveAnnotations(_)) 429 | )); 430 | } 431 | 432 | #[test] 433 | fn validate_unexpected_move_annotation() { 434 | let sgf = "(;BM[1])"; 435 | let node = &parse(sgf).unwrap()[0]; 436 | assert!(matches!( 437 | node.validate(), 438 | Err(InvalidNodeError::UnexpectedMoveAnnotation(_)) 439 | )); 440 | } 441 | 442 | #[test] 443 | fn validate_multiple_exclusive_annotations() { 444 | let sgf = "(;UC[2]GW[2])"; 445 | let node = &parse(sgf).unwrap()[0]; 446 | assert!(matches!( 447 | node.validate(), 448 | Err(InvalidNodeError::MultipleExclusiveAnnotations(_)) 449 | )); 450 | } 451 | 452 | #[test] 453 | fn validate_invalid_property() { 454 | let sgf = "(;BM[Invalid])"; 455 | let node = &parse(sgf).unwrap()[0]; 456 | assert!(matches!( 457 | node.validate(), 458 | Err(InvalidNodeError::InvalidProperty(_)) 459 | )); 460 | } 461 | } 462 | -------------------------------------------------------------------------------- /src/parser.rs: -------------------------------------------------------------------------------- 1 | use std::ptr::NonNull; 2 | 3 | use crate::go; 4 | use crate::lexer::{tokenize, LexerError, Token}; 5 | use crate::unknown_game; 6 | use crate::{GameTree, GameType, SgfNode, SgfProp}; 7 | 8 | /// Returns the [`GameTree`] values parsed from the provided text using default parsing options. 9 | /// 10 | /// This function will attempt to convert non-FF\[4\] files to FF\[4\] if possible. Check out 11 | /// [`parse_with_options`] if you want to change the default behavior. 12 | /// 13 | /// # Errors 14 | /// If the text can't be parsed as an SGF FF\[4\] collection, then an error is returned. 15 | /// 16 | /// # Examples 17 | /// ``` 18 | /// use sgf_parse::{parse, GameType}; 19 | /// 20 | /// let sgf = "(;SZ[9]C[Some comment];B[de];W[fe])(;B[de];W[ff])"; 21 | /// let gametrees = parse(sgf).unwrap(); 22 | /// assert!(gametrees.len() == 2); 23 | /// assert!(gametrees.iter().all(|gametree| gametree.gametype() == GameType::Go)); 24 | /// ``` 25 | pub fn parse(text: &str) -> Result, SgfParseError> { 26 | parse_with_options(text, &ParseOptions::default()) 27 | } 28 | 29 | /// Returns the [`GameTree`] values parsed from the provided text. 30 | /// 31 | /// # Errors 32 | /// If the text can't be parsed as an SGF FF\[4\] collection, then an error is returned. 33 | /// 34 | /// # Examples 35 | /// ``` 36 | /// use sgf_parse::{parse_with_options, ParseOptions, GameType, SgfParseError}; 37 | /// 38 | /// // Default options 39 | /// let sgf = "(;SZ[9]C[Some comment];B[de];W[fe])(;B[de];W[ff])"; 40 | /// let gametrees = parse_with_options(sgf, &ParseOptions::default()).unwrap(); 41 | /// assert!(gametrees.len() == 2); 42 | /// assert!(gametrees.iter().all(|gametree| gametree.gametype() == GameType::Go)); 43 | /// 44 | /// // Strict FF[4] identifiers 45 | /// let sgf = "(;SZ[9]CoPyright[Julian Andrews 2025];B[de];W[fe])(;B[de];W[ff])"; 46 | /// let parse_options = ParseOptions { 47 | /// convert_mixed_case_identifiers: false, 48 | /// ..ParseOptions::default() 49 | /// }; 50 | /// let result = parse_with_options(sgf, &parse_options); 51 | /// assert_eq!(result, Err(SgfParseError::InvalidFF4Property)); 52 | /// ``` 53 | pub fn parse_with_options( 54 | text: &str, 55 | options: &ParseOptions, 56 | ) -> Result, SgfParseError> { 57 | let text = text.trim(); 58 | let tokens = if options.lenient { 59 | tokenize(text) 60 | .take_while(Result::is_ok) 61 | .map(|result| result.unwrap().0) 62 | .collect() 63 | } else { 64 | tokenize(text) 65 | .map(|result| match result { 66 | Err(e) => Err(SgfParseError::LexerError(e)), 67 | Ok((token, _span)) => Ok(token), 68 | }) 69 | .collect::, _>>()? 70 | }; 71 | split_by_gametree(&tokens, options.lenient)? 72 | .into_iter() 73 | .map(|tokens| { 74 | let gametype = if options.lenient { 75 | find_gametype(tokens).unwrap_or(GameType::Go) 76 | } else { 77 | find_gametype(tokens)? 78 | }; 79 | match gametype { 80 | GameType::Go => parse_gametree::(tokens, options), 81 | GameType::Unknown => parse_gametree::(tokens, options), 82 | } 83 | }) 84 | .collect::>() 85 | } 86 | 87 | /// Options for parsing SGF files. 88 | /// 89 | /// # Examples 90 | /// See [`parse_with_options`] for usage examples. 91 | pub struct ParseOptions { 92 | /// Whether to allow conversion of FF\[3\] mixed case identifiers to FF\[4\]. 93 | /// 94 | /// All lower case letters are dropped. 95 | /// This should allow parsing any older files which are valid, but not valid FF\[4\]. 96 | pub convert_mixed_case_identifiers: bool, 97 | /// Whether to use lenient parsing. 98 | /// 99 | /// In lenient mode, the parser should never return an error, but will instead parse the SGF 100 | /// until it hits an error, and then return whatever it's managed to parse. 101 | pub lenient: bool, 102 | } 103 | 104 | impl Default for ParseOptions { 105 | fn default() -> Self { 106 | ParseOptions { 107 | convert_mixed_case_identifiers: true, 108 | lenient: false, 109 | } 110 | } 111 | } 112 | 113 | /// Error type for failures parsing sgf from text. 114 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 115 | pub enum SgfParseError { 116 | LexerError(LexerError), 117 | UnexpectedGameTreeStart, 118 | UnexpectedGameTreeEnd, 119 | UnexpectedProperty, 120 | UnexpectedEndOfData, 121 | UnexpectedGameType, 122 | InvalidFF4Property, 123 | InvalidGameType, 124 | NoGameTrees, 125 | } 126 | 127 | impl From for SgfParseError { 128 | fn from(error: LexerError) -> Self { 129 | Self::LexerError(error) 130 | } 131 | } 132 | 133 | impl std::fmt::Display for SgfParseError { 134 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 135 | match self { 136 | SgfParseError::LexerError(e) => write!(f, "Error tokenizing: {e}"), 137 | SgfParseError::UnexpectedGameTreeStart => write!(f, "Unexpected start of game tree"), 138 | SgfParseError::UnexpectedGameTreeEnd => write!(f, "Unexpected end of game tree"), 139 | SgfParseError::UnexpectedProperty => write!(f, "Unexpected property"), 140 | SgfParseError::UnexpectedEndOfData => write!(f, "Unexpected end of data"), 141 | SgfParseError::UnexpectedGameType => write!(f, "Unexpected game type"), 142 | SgfParseError::InvalidFF4Property => { 143 | write!( 144 | f, 145 | "Invalid FF[4] property without `convert_mixed_case_identifiers`" 146 | ) 147 | } 148 | SgfParseError::InvalidGameType => write!(f, "Invalid game type"), 149 | SgfParseError::NoGameTrees => write!(f, "No game trees found"), 150 | } 151 | } 152 | } 153 | 154 | impl std::error::Error for SgfParseError {} 155 | 156 | // Split the tokens up into individual gametrees. 157 | // 158 | // This will let us easily scan each gametree for GM properties. 159 | // Only considers StartGameTree/EndGameTree tokens. 160 | fn split_by_gametree(tokens: &[Token], lenient: bool) -> Result, SgfParseError> { 161 | let mut gametrees = vec![]; 162 | let mut gametree_depth: u64 = 0; 163 | let mut slice_start = 0; 164 | for (i, token) in tokens.iter().enumerate() { 165 | match token { 166 | Token::StartGameTree => gametree_depth += 1, 167 | Token::EndGameTree => { 168 | if gametree_depth == 0 { 169 | if lenient { 170 | break; 171 | } else { 172 | return Err(SgfParseError::UnexpectedGameTreeEnd); 173 | } 174 | } 175 | gametree_depth -= 1; 176 | if gametree_depth == 0 { 177 | gametrees.push(&tokens[slice_start..=i]); 178 | slice_start = i + 1; 179 | } 180 | } 181 | _ => {} 182 | } 183 | } 184 | if gametree_depth != 0 { 185 | if lenient { 186 | // For lenient parsing assume all remaining tokens are part of the last gametree. 187 | gametrees.push(&tokens[slice_start..]); 188 | } else { 189 | return Err(SgfParseError::UnexpectedEndOfData); 190 | } 191 | } 192 | if gametrees.is_empty() { 193 | if lenient { 194 | // For a valid SGF there needs to be at least one gametree. If we didn't fine one but we're 195 | // using lenient mode, just populate an empty gametree. 196 | gametrees.push(&[]); 197 | } else { 198 | return Err(SgfParseError::NoGameTrees); 199 | } 200 | } 201 | 202 | Ok(gametrees) 203 | } 204 | 205 | // Parse a single gametree of a known type. 206 | fn parse_gametree( 207 | tokens: &[Token], 208 | options: &ParseOptions, 209 | ) -> Result 210 | where 211 | SgfNode: std::convert::Into, 212 | { 213 | // TODO: Rewrite this without `unsafe` 214 | let mut collection: Vec> = vec![]; 215 | // //// Pointer to the `Vec` of children we're currently building. 216 | let mut current_node_list_ptr = NonNull::new(&mut collection).unwrap(); 217 | // Stack of pointers to incomplete `Vec`s of children. 218 | let mut incomplete_child_lists: Vec>>> = vec![]; 219 | //// Using pointers involves some unsafe calls, but should be ok here. 220 | //// Since pointers are always initialized from real structs, and those structs 221 | //// live for the whole function body, our only safety concern is dangling pointers. 222 | //// 223 | //// Since we build the tree traversing depth-first those structs shouldn't be 224 | //// modified while the pointer is live. Heap-allocated contents of their 225 | //// `children` may be modified, but that shouldn't change anything. 226 | 227 | let mut tokens = tokens.iter().peekable(); 228 | while let Some(token) = tokens.next() { 229 | match token { 230 | Token::StartGameTree => { 231 | // SGF game trees must have a root node. 232 | if let Some(node_list_ptr) = incomplete_child_lists.last() { 233 | let node_list = unsafe { node_list_ptr.as_ref() }; 234 | if node_list.is_empty() { 235 | if options.lenient { 236 | break; 237 | } else { 238 | return Err(SgfParseError::UnexpectedGameTreeStart); 239 | } 240 | } 241 | } 242 | incomplete_child_lists.push(current_node_list_ptr); 243 | } 244 | Token::EndGameTree => match incomplete_child_lists.pop() { 245 | Some(node_list) => current_node_list_ptr = node_list, 246 | None => { 247 | if options.lenient { 248 | break; 249 | } else { 250 | return Err(SgfParseError::UnexpectedGameTreeEnd); 251 | } 252 | } 253 | }, 254 | Token::StartNode => { 255 | let mut new_node = SgfNode::default(); 256 | let mut prop_tokens = vec![]; 257 | while let Some(Token::Property(_)) = tokens.peek() { 258 | prop_tokens.push(tokens.next().unwrap()); 259 | } 260 | for token in prop_tokens { 261 | match token { 262 | // TODO: Consider refactoring to consume tokens and clone of values. 263 | Token::Property((identifier, values)) => { 264 | let identifier = { 265 | if identifier.chars().all(|c| c.is_ascii_uppercase()) { 266 | identifier.clone() 267 | } else if options.convert_mixed_case_identifiers { 268 | identifier 269 | .chars() 270 | .filter(|c| c.is_ascii_uppercase()) 271 | .collect() 272 | } else if options.lenient { 273 | break; 274 | } else { 275 | return Err(SgfParseError::InvalidFF4Property); 276 | } 277 | }; 278 | new_node 279 | .properties 280 | .push(Prop::new(identifier, values.clone())) 281 | } 282 | _ => unreachable!(), 283 | } 284 | } 285 | let node_list = unsafe { current_node_list_ptr.as_mut() }; 286 | node_list.push(new_node); 287 | current_node_list_ptr = 288 | NonNull::new(&mut node_list.last_mut().unwrap().children).unwrap(); 289 | } 290 | Token::Property(_) => { 291 | if options.lenient { 292 | break; 293 | } else { 294 | return Err(SgfParseError::UnexpectedProperty); 295 | } 296 | } 297 | } 298 | } 299 | 300 | if !options.lenient && (!incomplete_child_lists.is_empty() || collection.len() != 1) { 301 | return Err(SgfParseError::UnexpectedEndOfData); 302 | } 303 | let mut root_node = if options.lenient { 304 | // A valid game tree must have at least a single (empty) node. So make one! 305 | collection.into_iter().next().unwrap_or_default() 306 | } else { 307 | collection 308 | .into_iter() 309 | .next() 310 | .ok_or(SgfParseError::UnexpectedEndOfData)? 311 | }; 312 | root_node.is_root = true; 313 | Ok(root_node.into()) 314 | } 315 | 316 | // Figure out which game to parse from a slice of tokens. 317 | // 318 | // This function is necessary because we need to know the game before we can do the parsing. 319 | fn find_gametype(tokens: &[Token]) -> Result { 320 | match find_gametree_root_prop_values("GM", tokens)?.map(|v| v.as_slice()) { 321 | None => Ok(GameType::Go), 322 | Some([value]) if value.parse::().is_ok() => match value.as_str() { 323 | "1" => Ok(GameType::Go), 324 | _ => Ok(GameType::Unknown), 325 | }, 326 | _ => Err(SgfParseError::InvalidGameType), 327 | } 328 | } 329 | 330 | // Find the property values for a given identifier in the root node from the gametree's tokens. 331 | // 332 | // We use this to determine key root properties (like GM and FF) before parsing. 333 | // Returns an error if there's more than one match. 334 | fn find_gametree_root_prop_values<'a>( 335 | prop_ident: &'a str, 336 | tokens: &'a [Token], 337 | ) -> Result>, SgfParseError> { 338 | // Find the matching property values in the first node. 339 | // Skip the initial StartGameTree, StartNode tokens; we'll handle any errors later. 340 | let matching_tokens: Vec<&Vec> = tokens 341 | .iter() 342 | .skip(2) 343 | .take_while(|&token| matches!(token, Token::Property(_))) 344 | .filter_map(move |token| match token { 345 | Token::Property((ident, values)) if ident == prop_ident => Some(values), 346 | _ => None, 347 | }) 348 | .collect(); 349 | 350 | match matching_tokens.len() { 351 | 0 => Ok(None), 352 | 1 => Ok(Some(matching_tokens[0])), 353 | _ => Err(SgfParseError::UnexpectedProperty), 354 | } 355 | } 356 | 357 | #[cfg(test)] 358 | mod test { 359 | use super::*; 360 | use crate::{go, serialize}; 361 | 362 | fn load_test_sgf() -> Result> { 363 | // See https://www.red-bean.com/sgf/examples/ 364 | let mut sgf_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); 365 | sgf_path.push("resources/test/ff4_ex.sgf"); 366 | let data = std::fs::read_to_string(sgf_path)?; 367 | 368 | Ok(data) 369 | } 370 | 371 | fn get_go_nodes() -> Result>, Box> { 372 | let data = load_test_sgf()?; 373 | 374 | Ok(go::parse(&data)?) 375 | } 376 | 377 | fn node_depth(mut sgf_node: &SgfNode) -> u64 { 378 | let mut depth = 1; 379 | while sgf_node.children().count() > 0 { 380 | depth += 1; 381 | sgf_node = sgf_node.children().next().unwrap(); 382 | } 383 | depth 384 | } 385 | 386 | #[test] 387 | fn sgf_has_two_gametrees() { 388 | let sgf_nodes = get_go_nodes().unwrap(); 389 | assert_eq!(sgf_nodes.len(), 2); 390 | } 391 | 392 | #[test] 393 | fn gametree_one_has_five_variations() { 394 | let sgf_nodes = get_go_nodes().unwrap(); 395 | let sgf_node = &sgf_nodes[0]; 396 | assert_eq!(sgf_node.children().count(), 5); 397 | } 398 | 399 | #[test] 400 | fn gametree_one_has_size_19() { 401 | let sgf_nodes = get_go_nodes().unwrap(); 402 | let sgf_node = &sgf_nodes[0]; 403 | match sgf_node.get_property("SZ") { 404 | Some(go::Prop::SZ(size)) => assert_eq!(size, &(19, 19)), 405 | _ => unreachable!("Expected size property"), 406 | } 407 | } 408 | 409 | #[test] 410 | fn gametree_variation_depths() { 411 | let sgf_nodes = get_go_nodes().unwrap(); 412 | let sgf_node = &sgf_nodes[0]; 413 | let children: Vec<_> = sgf_node.children().collect(); 414 | assert_eq!(node_depth(children[0]), 13); 415 | assert_eq!(node_depth(children[1]), 4); 416 | assert_eq!(node_depth(children[2]), 4); 417 | } 418 | 419 | #[test] 420 | fn gametree_two_has_one_variation() { 421 | let sgf_nodes = get_go_nodes().unwrap(); 422 | let sgf_node = &sgf_nodes[1]; 423 | assert_eq!(sgf_node.children().count(), 1); 424 | } 425 | 426 | #[test] 427 | fn serialize_then_parse() { 428 | let data = load_test_sgf().unwrap(); 429 | let gametrees = parse(&data).unwrap(); 430 | let text = serialize(&gametrees); 431 | assert_eq!(gametrees, parse(&text).unwrap()); 432 | } 433 | 434 | #[test] 435 | fn invalid_property() { 436 | let input = "(;GM[1]W[rp.pmonpoqprpsornqmpm])"; 437 | let sgf_nodes = go::parse(input).unwrap(); 438 | let expected = vec![ 439 | go::Prop::GM(1), 440 | go::Prop::Invalid("W".to_string(), vec!["rp.pmonpoqprpsornqmpm".to_string()]), 441 | ]; 442 | 443 | assert_eq!(sgf_nodes.len(), 1); 444 | let sgf_node = &sgf_nodes[0]; 445 | assert_eq!(sgf_node.properties().cloned().collect::>(), expected); 446 | } 447 | 448 | #[test] 449 | fn unknown_game() { 450 | let input = "(;GM[37]W[rp.pmonpoqprpsornqmpm])"; 451 | let gametrees = parse(input).unwrap(); 452 | assert_eq!(gametrees.len(), 1); 453 | assert_eq!(gametrees[0].gametype(), GameType::Unknown); 454 | let sgf_node = match &gametrees[0] { 455 | GameTree::Unknown(node) => node, 456 | _ => panic!("Unexpected game type"), 457 | }; 458 | let expected = vec![ 459 | unknown_game::Prop::GM(37), 460 | unknown_game::Prop::W("rp.pmonpoqprpsornqmpm".into()), 461 | ]; 462 | 463 | assert_eq!(sgf_node.properties().cloned().collect::>(), expected); 464 | } 465 | 466 | #[test] 467 | fn mixed_games() { 468 | let input = "(;GM[1];W[dd])(;GM[37]W[rp.pmonpoqprpsornqmpm])"; 469 | let gametrees = parse(input).unwrap(); 470 | assert_eq!(gametrees.len(), 2); 471 | assert_eq!(gametrees[0].gametype(), GameType::Go); 472 | assert_eq!(gametrees[1].gametype(), GameType::Unknown); 473 | } 474 | 475 | #[test] 476 | fn stack_overflow() { 477 | // This input generated a stack overflow with the old code 478 | let input = "(;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;)"; 479 | let result = parse(&input); 480 | assert!(result.is_ok()); 481 | } 482 | 483 | #[test] 484 | fn converts_up_ff3_property() { 485 | let input = "(;GM[1]FF[3]CoPyright[test])"; 486 | let expected = vec![ 487 | go::Prop::GM(1), 488 | go::Prop::FF(3), 489 | go::Prop::CP("test".into()), 490 | ]; 491 | 492 | let sgf_nodes = go::parse(input).unwrap(); 493 | 494 | assert_eq!(sgf_nodes.len(), 1); 495 | let properties = sgf_nodes[0].properties().cloned().collect::>(); 496 | assert_eq!(properties, expected); 497 | } 498 | 499 | #[test] 500 | fn doesnt_convert_if_not_allowed() { 501 | let input = "(;GM[1]FF[3]CoPyright[test])"; 502 | let parse_options = ParseOptions { 503 | convert_mixed_case_identifiers: false, 504 | ..ParseOptions::default() 505 | }; 506 | let result = parse_with_options(input, &parse_options); 507 | assert_eq!(result, Err(SgfParseError::InvalidFF4Property)); 508 | } 509 | 510 | #[test] 511 | fn compressed_list_for_unknown_game() { 512 | let input = "(;GM[127]MA[a:b])"; 513 | let gametree = parse(&input).unwrap().pop().unwrap(); 514 | let node = match gametree { 515 | GameTree::Unknown(node) => node, 516 | _ => panic!("Expected Unknown Game type"), 517 | }; 518 | match node.get_property("MA") { 519 | Some(unknown_game::Prop::MA(values)) => { 520 | assert_eq!(values.len(), 1); 521 | assert!(values.contains("a:b")); 522 | } 523 | _ => panic!("MA prop not found"), 524 | } 525 | } 526 | 527 | #[test] 528 | fn strips_whitespace() { 529 | let input = "\n(;GM[1];B[cc])"; 530 | let sgf_nodes = go::parse(&input).unwrap(); 531 | assert_eq!(sgf_nodes.len(), 1); 532 | } 533 | 534 | #[test] 535 | fn lenient_parsing_unclosed_parens_ok() { 536 | let input = "\n(;GM[1];B[cc]"; 537 | let parse_options = ParseOptions { 538 | lenient: true, 539 | ..ParseOptions::default() 540 | }; 541 | let game_trees = parse_with_options(input, &parse_options).unwrap(); 542 | assert_eq!(game_trees.len(), 1); 543 | } 544 | 545 | #[test] 546 | fn lenient_parsing_ignores_trailing_garbage() { 547 | let input = "\n(;GM[1];B[cc]))"; 548 | let parse_options = ParseOptions { 549 | lenient: true, 550 | ..ParseOptions::default() 551 | }; 552 | let game_trees = parse_with_options(input, &parse_options).unwrap(); 553 | assert_eq!(game_trees.len(), 1); 554 | } 555 | 556 | #[test] 557 | fn lenient_parsing_handles_unescaped_property_end() { 558 | let input = "(;B[cc];W[dd];C[username [12k]: foo])"; 559 | let parse_options = ParseOptions { 560 | lenient: true, 561 | ..ParseOptions::default() 562 | }; 563 | let game_trees = parse_with_options(input, &parse_options).unwrap(); 564 | assert_eq!(game_trees.len(), 1); 565 | let sgf_node = game_trees[0].as_go_node().unwrap(); 566 | // Should parse up through "[12k]" successfully 567 | assert_eq!(sgf_node.main_variation().count(), 3); 568 | } 569 | 570 | #[test] 571 | fn lenient_parsing_handles_unclosed_property_value() { 572 | let input = "(;B[cc];W[dd];B[ee"; 573 | let parse_options = ParseOptions { 574 | lenient: true, 575 | ..ParseOptions::default() 576 | }; 577 | let game_trees = parse_with_options(input, &parse_options).unwrap(); 578 | assert_eq!(game_trees.len(), 1); 579 | let sgf_node = game_trees[0].as_go_node().unwrap(); 580 | // Should find 3 nodes. The last unfinished node has no properties since "B[ee" is unclosed. 581 | assert_eq!(sgf_node.main_variation().count(), 3); 582 | assert_eq!( 583 | sgf_node.main_variation().last().unwrap().properties.len(), 584 | 0 585 | ); 586 | } 587 | 588 | #[test] 589 | fn lenient_parsing_handles_missing_property_value() { 590 | let input = "(;B[cc];W[dd];B"; 591 | let parse_options = ParseOptions { 592 | lenient: true, 593 | ..ParseOptions::default() 594 | }; 595 | let game_trees = parse_with_options(input, &parse_options).unwrap(); 596 | assert_eq!(game_trees.len(), 1); 597 | let sgf_node = game_trees[0].as_go_node().unwrap(); 598 | // Should find 3 nodes. The last node has no properties since "B" is missing its value 599 | assert_eq!(sgf_node.main_variation().count(), 3); 600 | assert_eq!( 601 | sgf_node.main_variation().last().unwrap().properties.len(), 602 | 0 603 | ); 604 | } 605 | 606 | #[test] 607 | fn lenient_parsing_handles_missing_first_node_start() { 608 | let input = "(B[cc])"; 609 | let parse_options = ParseOptions { 610 | lenient: true, 611 | ..ParseOptions::default() 612 | }; 613 | let game_trees = parse_with_options(input, &parse_options).unwrap(); 614 | assert_eq!(game_trees.len(), 1); 615 | let sgf_node = game_trees[0].as_go_node().unwrap(); 616 | // A single empty node. 617 | assert_eq!(sgf_node.main_variation().count(), 1); 618 | assert_eq!( 619 | sgf_node.main_variation().last().unwrap().properties.len(), 620 | 0 621 | ); 622 | } 623 | } 624 | -------------------------------------------------------------------------------- /src/prop_macro.rs: -------------------------------------------------------------------------------- 1 | macro_rules! sgf_prop { 2 | ($name:ident, $mv:ty, $pt:ty, $st:ty, { $($variants:tt)* }) => { 3 | /// An SGF Property with identifier and value. 4 | /// 5 | /// All [general properties](https://www.red-bean.com/sgf/properties.html) from the SGF 6 | /// specification and all game specific properties will return the approprite enum 7 | /// instance with parsed data. Unrecognized properties will return 8 | /// [`Prop::Unknown`](`Self::Unknown`). Recognized general or game specific properties with invalid values will 9 | /// return [`Prop::Invalid`](`Self::Invalid`). 10 | /// 11 | /// See [property value types](https://www.red-bean.com/sgf/sgf4.html#types) for a list of types 12 | /// recognized by SGF. For parsing purposes the following mappings are used: 13 | /// * 'Number' => [`i64`] 14 | /// * 'Real' => [`f64`] 15 | /// * 'Double' => [`Double`](`crate::props::Double`) 16 | /// * 'Color' => [`Color`](`crate::props::Color`) 17 | /// * 'SimpleText' => [`SimpleText`](`crate::props::SimpleText`) 18 | /// * 'Text' => [`Text`](`crate::props::Text`) 19 | /// * 'Point' => [`Point`](`Self::Point`) 20 | /// * 'Stone' => [`Stone`](`Self::Stone`) 21 | /// * 'Move' => [`Move`](`Self::Move`) 22 | /// * 'List' => [`HashSet`](`std::collections::HashSet`) 23 | /// * 'Compose' => [`tuple`] of the composed values 24 | #[derive(Clone, Debug, PartialEq)] 25 | pub enum $name { 26 | // Move properties 27 | B($mv), 28 | KO, 29 | MN(i64), 30 | W($mv), 31 | // Setup properties 32 | AB(std::collections::HashSet<$st>), 33 | AE(std::collections::HashSet<$pt>), 34 | AW(std::collections::HashSet<$st>), 35 | PL(crate::props::Color), 36 | // Node annotation properties 37 | C(crate::props::Text), 38 | DM(crate::props::Double), 39 | GB(crate::props::Double), 40 | GW(crate::props::Double), 41 | HO(crate::props::Double), 42 | N(crate::props::SimpleText), 43 | UC(crate::props::Double), 44 | V(f64), 45 | // Move annotation properties 46 | BM(crate::props::Double), 47 | DO, 48 | IT, 49 | TE(crate::props::Double), 50 | // Markup properties 51 | AR(std::collections::HashSet<($pt, $pt)>), 52 | CR(std::collections::HashSet<$pt>), 53 | DD(std::collections::HashSet<$pt>), 54 | LB(std::collections::HashSet<($pt, crate::props::SimpleText)>), 55 | LN(std::collections::HashSet<($pt, $pt)>), 56 | MA(std::collections::HashSet<$pt>), 57 | SL(std::collections::HashSet<$pt>), 58 | SQ(std::collections::HashSet<$pt>), 59 | TR(std::collections::HashSet<$pt>), 60 | // Root properties 61 | AP((crate::props::SimpleText, crate::props::SimpleText)), 62 | CA(crate::props::SimpleText), 63 | FF(i64), 64 | GM(i64), 65 | ST(i64), 66 | SZ((u8, u8)), 67 | // Game info properties 68 | AN(crate::props::SimpleText), 69 | BR(crate::props::SimpleText), 70 | BT(crate::props::SimpleText), 71 | CP(crate::props::SimpleText), 72 | DT(crate::props::SimpleText), 73 | EV(crate::props::SimpleText), 74 | GN(crate::props::SimpleText), 75 | GC(crate::props::Text), 76 | ON(crate::props::SimpleText), 77 | OT(crate::props::SimpleText), 78 | PB(crate::props::SimpleText), 79 | PC(crate::props::SimpleText), 80 | PW(crate::props::SimpleText), 81 | RE(crate::props::SimpleText), 82 | RO(crate::props::SimpleText), 83 | RU(crate::props::SimpleText), 84 | SO(crate::props::SimpleText), 85 | TM(f64), 86 | US(crate::props::SimpleText), 87 | WR(crate::props::SimpleText), 88 | WT(crate::props::SimpleText), 89 | // Timing properties 90 | BL(f64), 91 | OB(i64), 92 | OW(i64), 93 | WL(f64), 94 | // Miscellaneous properties 95 | FG(Option<(i64, crate::props::SimpleText)>), 96 | PM(i64), 97 | VW(std::collections::HashSet<$pt>), 98 | Unknown(String, Vec), 99 | Invalid(String, Vec), 100 | // Game specific properties 101 | $($variants)* 102 | } 103 | 104 | impl $name { 105 | fn parse_general_prop(identifier: String, values: Vec) -> Self { 106 | use crate::props::parse::{ 107 | parse_elist, parse_list, parse_list_composed, parse_single_value, verify_empty, 108 | }; 109 | 110 | let result = match &identifier[..] { 111 | "B" => parse_single_value(&values).map(Self::B), 112 | "KO" => verify_empty(&values).map(|()| Self::KO), 113 | "MN" => parse_single_value(&values).map(Self::MN), 114 | "W" => parse_single_value(&values).map(Self::W), 115 | "AB" => parse_list(&values).map(Self::AB), 116 | "AE" => parse_list(&values).map(Self::AE), 117 | "AW" => parse_list(&values).map(Self::AW), 118 | "PL" => parse_single_value(&values).map(Self::PL), 119 | "C" => parse_single_value(&values).map(Self::C), 120 | "DM" => parse_single_value(&values).map(Self::DM), 121 | "GB" => parse_single_value(&values).map(Self::GB), 122 | "GW" => parse_single_value(&values).map(Self::GW), 123 | "HO" => parse_single_value(&values).map(Self::HO), 124 | "N" => parse_single_value(&values).map(Self::N), 125 | "UC" => parse_single_value(&values).map(Self::UC), 126 | "V" => parse_single_value(&values).map(Self::V), 127 | "DO" => verify_empty(&values).map(|()| Self::DO), 128 | "IT" => verify_empty(&values).map(|()| Self::IT), 129 | "BM" => parse_single_value(&values).map(Self::BM), 130 | "TE" => parse_single_value(&values).map(Self::TE), 131 | "AR" => parse_list_composed(&values).map(Self::AR), 132 | "CR" => parse_list(&values).map(Self::CR), 133 | "DD" => parse_elist(&values).map(Self::DD), 134 | "LB" => parse_labels(&values).map(Self::LB), 135 | "LN" => parse_list_composed(&values).map(Self::LN), 136 | "MA" => parse_list(&values).map(Self::MA), 137 | "SL" => parse_list(&values).map(Self::SL), 138 | "SQ" => parse_list(&values).map(Self::SQ), 139 | "TR" => parse_list(&values).map(Self::TR), 140 | "AP" => parse_application(&values).map(Self::AP), 141 | "CA" => parse_single_value(&values).map(Self::CA), 142 | "FF" => match parse_single_value(&values) { 143 | Ok(value) => { 144 | if !(0..=4).contains(&value) { 145 | Err(SgfPropError {}) 146 | } else { 147 | Ok(Self::FF(value)) 148 | } 149 | } 150 | _ => Err(SgfPropError {}), 151 | }, 152 | "GM" => parse_single_value(&values).map(Self::GM), 153 | "ST" => match parse_single_value(&values) { 154 | Ok(value) => { 155 | if !(0..=3).contains(&value) { 156 | Err(SgfPropError {}) 157 | } else { 158 | Ok(Self::ST(value)) 159 | } 160 | } 161 | _ => Err(SgfPropError {}), 162 | }, 163 | "SZ" => parse_size(&values).map(Self::SZ), 164 | "AN" => parse_single_value(&values).map(Self::AN), 165 | "BR" => parse_single_value(&values).map(Self::BR), 166 | "BT" => parse_single_value(&values).map(Self::BT), 167 | "CP" => parse_single_value(&values).map(Self::CP), 168 | "DT" => parse_single_value(&values).map(Self::DT), 169 | "EV" => parse_single_value(&values).map(Self::EV), 170 | "GN" => parse_single_value(&values).map(Self::GN), 171 | "GC" => parse_single_value(&values).map(Self::GC), 172 | "ON" => parse_single_value(&values).map(Self::ON), 173 | "OT" => parse_single_value(&values).map(Self::OT), 174 | "PB" => parse_single_value(&values).map(Self::PB), 175 | "PC" => parse_single_value(&values).map(Self::PC), 176 | "PW" => parse_single_value(&values).map(Self::PW), 177 | "RE" => parse_single_value(&values).map(Self::RE), 178 | "RO" => parse_single_value(&values).map(Self::RO), 179 | "RU" => parse_single_value(&values).map(Self::RU), 180 | "SO" => parse_single_value(&values).map(Self::SO), 181 | "TM" => parse_single_value(&values).map(Self::TM), 182 | "US" => parse_single_value(&values).map(Self::US), 183 | "WR" => parse_single_value(&values).map(Self::WR), 184 | "WT" => parse_single_value(&values).map(Self::WT), 185 | "BL" => parse_single_value(&values).map(Self::BL), 186 | "OB" => parse_single_value(&values).map(Self::OB), 187 | "OW" => parse_single_value(&values).map(Self::OW), 188 | "WL" => parse_single_value(&values).map(Self::WL), 189 | "FG" => parse_figure(&values).map(Self::FG), 190 | "PM" => match parse_single_value(&values) { 191 | Ok(value) => { 192 | if !(1..=2).contains(&value) { 193 | Err(SgfPropError {}) 194 | } else { 195 | Ok(Self::PM(value)) 196 | } 197 | } 198 | _ => Err(SgfPropError {}), 199 | }, 200 | "VW" => parse_elist(&values).map(Self::VW), 201 | _ => return Self::Unknown(identifier, values), 202 | }; 203 | result.unwrap_or(Self::Invalid(identifier, values)) 204 | } 205 | 206 | fn general_identifier(&self) -> Option { 207 | match self { 208 | Self::B(_) => Some("B".to_string()), 209 | Self::KO => Some("KO".to_string()), 210 | Self::MN(_) => Some("MN".to_string()), 211 | Self::W(_) => Some("W".to_string()), 212 | Self::AB(_) => Some("AB".to_string()), 213 | Self::AE(_) => Some("AE".to_string()), 214 | Self::AW(_) => Some("AW".to_string()), 215 | Self::PL(_) => Some("PL".to_string()), 216 | Self::C(_) => Some("C".to_string()), 217 | Self::DM(_) => Some("DM".to_string()), 218 | Self::GB(_) => Some("GB".to_string()), 219 | Self::GW(_) => Some("GW".to_string()), 220 | Self::HO(_) => Some("HO".to_string()), 221 | Self::N(_) => Some("N".to_string()), 222 | Self::UC(_) => Some("UC".to_string()), 223 | Self::V(_) => Some("V".to_string()), 224 | Self::DO => Some("DO".to_string()), 225 | Self::IT => Some("IT".to_string()), 226 | Self::BM(_) => Some("BM".to_string()), 227 | Self::TE(_) => Some("TE".to_string()), 228 | Self::AR(_) => Some("AR".to_string()), 229 | Self::CR(_) => Some("CR".to_string()), 230 | Self::DD(_) => Some("DD".to_string()), 231 | Self::LB(_) => Some("LB".to_string()), 232 | Self::LN(_) => Some("LN".to_string()), 233 | Self::MA(_) => Some("MA".to_string()), 234 | Self::SL(_) => Some("SL".to_string()), 235 | Self::SQ(_) => Some("SQ".to_string()), 236 | Self::TR(_) => Some("TR".to_string()), 237 | Self::AP(_) => Some("AP".to_string()), 238 | Self::CA(_) => Some("CA".to_string()), 239 | Self::FF(_) => Some("FF".to_string()), 240 | Self::GM(_) => Some("GM".to_string()), 241 | Self::ST(_) => Some("ST".to_string()), 242 | Self::SZ(_) => Some("SZ".to_string()), 243 | Self::AN(_) => Some("AN".to_string()), 244 | Self::BR(_) => Some("BR".to_string()), 245 | Self::BT(_) => Some("BT".to_string()), 246 | Self::CP(_) => Some("CP".to_string()), 247 | Self::DT(_) => Some("DT".to_string()), 248 | Self::EV(_) => Some("EV".to_string()), 249 | Self::GN(_) => Some("GN".to_string()), 250 | Self::GC(_) => Some("GC".to_string()), 251 | Self::ON(_) => Some("ON".to_string()), 252 | Self::OT(_) => Some("OT".to_string()), 253 | Self::PB(_) => Some("PB".to_string()), 254 | Self::PC(_) => Some("PC".to_string()), 255 | Self::PW(_) => Some("PW".to_string()), 256 | Self::RE(_) => Some("RE".to_string()), 257 | Self::RO(_) => Some("RO".to_string()), 258 | Self::RU(_) => Some("RU".to_string()), 259 | Self::SO(_) => Some("SO".to_string()), 260 | Self::TM(_) => Some("TM".to_string()), 261 | Self::US(_) => Some("US".to_string()), 262 | Self::WR(_) => Some("WR".to_string()), 263 | Self::WT(_) => Some("WT".to_string()), 264 | Self::BL(_) => Some("BL".to_string()), 265 | Self::OB(_) => Some("OB".to_string()), 266 | Self::OW(_) => Some("OW".to_string()), 267 | Self::WL(_) => Some("WL".to_string()), 268 | Self::FG(_) => Some("FG".to_string()), 269 | Self::PM(_) => Some("PM".to_string()), 270 | Self::VW(_) => Some("VW".to_string()), 271 | Self::Invalid(identifier, _) => Some(identifier.to_string()), 272 | Self::Unknown(identifier, _) => Some(identifier.to_string()), 273 | #[allow(unreachable_patterns)] 274 | _ => None, 275 | } 276 | } 277 | 278 | fn general_property_type(&self) -> Option { 279 | match &self { 280 | Self::B(_) => Some(PropertyType::Move), 281 | Self::KO => Some(PropertyType::Move), 282 | Self::MN(_) => Some(PropertyType::Move), 283 | Self::W(_) => Some(PropertyType::Move), 284 | Self::AB(_) => Some(PropertyType::Setup), 285 | Self::AE(_) => Some(PropertyType::Setup), 286 | Self::AW(_) => Some(PropertyType::Setup), 287 | Self::PL(_) => Some(PropertyType::Setup), 288 | Self::DO => Some(PropertyType::Move), 289 | Self::IT => Some(PropertyType::Move), 290 | Self::BM(_) => Some(PropertyType::Move), 291 | Self::TE(_) => Some(PropertyType::Move), 292 | Self::DD(_) => Some(PropertyType::Inherit), 293 | Self::AP(_) => Some(PropertyType::Root), 294 | Self::CA(_) => Some(PropertyType::Root), 295 | Self::FF(_) => Some(PropertyType::Root), 296 | Self::GM(_) => Some(PropertyType::Root), 297 | Self::ST(_) => Some(PropertyType::Root), 298 | Self::SZ(_) => Some(PropertyType::Root), 299 | Self::AN(_) => Some(PropertyType::GameInfo), 300 | Self::BR(_) => Some(PropertyType::GameInfo), 301 | Self::BT(_) => Some(PropertyType::GameInfo), 302 | Self::CP(_) => Some(PropertyType::GameInfo), 303 | Self::DT(_) => Some(PropertyType::GameInfo), 304 | Self::EV(_) => Some(PropertyType::GameInfo), 305 | Self::GN(_) => Some(PropertyType::GameInfo), 306 | Self::GC(_) => Some(PropertyType::GameInfo), 307 | Self::ON(_) => Some(PropertyType::GameInfo), 308 | Self::OT(_) => Some(PropertyType::GameInfo), 309 | Self::PB(_) => Some(PropertyType::GameInfo), 310 | Self::PC(_) => Some(PropertyType::GameInfo), 311 | Self::PW(_) => Some(PropertyType::GameInfo), 312 | Self::RE(_) => Some(PropertyType::GameInfo), 313 | Self::RO(_) => Some(PropertyType::GameInfo), 314 | Self::RU(_) => Some(PropertyType::GameInfo), 315 | Self::SO(_) => Some(PropertyType::GameInfo), 316 | Self::TM(_) => Some(PropertyType::GameInfo), 317 | Self::US(_) => Some(PropertyType::GameInfo), 318 | Self::WR(_) => Some(PropertyType::GameInfo), 319 | Self::WT(_) => Some(PropertyType::GameInfo), 320 | Self::BL(_) => Some(PropertyType::Move), 321 | Self::OB(_) => Some(PropertyType::Move), 322 | Self::OW(_) => Some(PropertyType::Move), 323 | Self::WL(_) => Some(PropertyType::Move), 324 | Self::PM(_) => Some(PropertyType::Inherit), 325 | Self::VW(_) => Some(PropertyType::Inherit), 326 | _ => None, 327 | } 328 | } 329 | 330 | fn serialize_prop_value(&self) -> Option { 331 | match self { 332 | Self::B(x) => Some(x.to_sgf()), 333 | Self::KO => Some("".to_string()), 334 | Self::MN(x) => Some(x.to_sgf()), 335 | Self::W(x) => Some(x.to_sgf()), 336 | Self::AB(x) => Some(x.to_sgf()), 337 | Self::AE(x) => Some(x.to_sgf()), 338 | Self::AW(x) => Some(x.to_sgf()), 339 | Self::PL(x) => Some(x.to_sgf()), 340 | Self::C(x) => Some(x.to_sgf()), 341 | Self::DM(x) => Some(x.to_sgf()), 342 | Self::GB(x) => Some(x.to_sgf()), 343 | Self::GW(x) => Some(x.to_sgf()), 344 | Self::HO(x) => Some(x.to_sgf()), 345 | Self::N(x) => Some(x.to_sgf()), 346 | Self::UC(x) => Some(x.to_sgf()), 347 | Self::V(x) => Some(x.to_sgf()), 348 | Self::AR(x) => Some(x.to_sgf()), 349 | Self::CR(x) => Some(x.to_sgf()), 350 | Self::DO => Some("".to_string()), 351 | Self::IT => Some("".to_string()), 352 | Self::BM(x) => Some(x.to_sgf()), 353 | Self::TE(x) => Some(x.to_sgf()), 354 | Self::DD(x) => Some(x.to_sgf()), 355 | Self::LB(x) => Some(x.to_sgf()), 356 | Self::LN(x) => Some(x.to_sgf()), 357 | Self::MA(x) => Some(x.to_sgf()), 358 | Self::SL(x) => Some(x.to_sgf()), 359 | Self::SQ(x) => Some(x.to_sgf()), 360 | Self::TR(x) => Some(x.to_sgf()), 361 | Self::AP(x) => Some(x.to_sgf()), 362 | Self::CA(x) => Some(x.to_sgf()), 363 | Self::FF(x) => Some(x.to_sgf()), 364 | Self::GM(x) => Some(x.to_sgf()), 365 | Self::ST(x) => Some(x.to_sgf()), 366 | Self::SZ(x) => Some(x.to_sgf()), 367 | Self::AN(x) => Some(x.to_sgf()), 368 | Self::BR(x) => Some(x.to_sgf()), 369 | Self::BT(x) => Some(x.to_sgf()), 370 | Self::CP(x) => Some(x.to_sgf()), 371 | Self::DT(x) => Some(x.to_sgf()), 372 | Self::EV(x) => Some(x.to_sgf()), 373 | Self::GN(x) => Some(x.to_sgf()), 374 | Self::GC(x) => Some(x.to_sgf()), 375 | Self::ON(x) => Some(x.to_sgf()), 376 | Self::OT(x) => Some(x.to_sgf()), 377 | Self::PB(x) => Some(x.to_sgf()), 378 | Self::PC(x) => Some(x.to_sgf()), 379 | Self::PW(x) => Some(x.to_sgf()), 380 | Self::RE(x) => Some(x.to_sgf()), 381 | Self::RO(x) => Some(x.to_sgf()), 382 | Self::RU(x) => Some(x.to_sgf()), 383 | Self::SO(x) => Some(x.to_sgf()), 384 | Self::TM(x) => Some(x.to_sgf()), 385 | Self::US(x) => Some(x.to_sgf()), 386 | Self::WR(x) => Some(x.to_sgf()), 387 | Self::WT(x) => Some(x.to_sgf()), 388 | Self::BL(x) => Some(x.to_sgf()), 389 | Self::OB(x) => Some(x.to_sgf()), 390 | Self::OW(x) => Some(x.to_sgf()), 391 | Self::WL(x) => Some(x.to_sgf()), 392 | Self::FG(x) => Some(x.to_sgf()), 393 | Self::PM(x) => Some(x.to_sgf()), 394 | Self::VW(x) => Some(x.to_sgf()), 395 | Self::Unknown(_, x) => Some(x.to_sgf()), 396 | Self::Invalid(_, x) => Some(x.to_sgf()), 397 | #[allow(unreachable_patterns)] 398 | _ => None, 399 | } 400 | } 401 | 402 | fn general_validate_properties(properties: &[Self], is_root: bool) -> Result<(), crate::InvalidNodeError> { 403 | use crate::InvalidNodeError; 404 | let mut identifiers = HashSet::new(); 405 | let mut markup_points = HashSet::new(); 406 | let mut setup_node = false; 407 | let mut move_node = false; 408 | let mut move_seen = false; 409 | let mut exclusive_node_annotations = 0; 410 | let mut move_annotation_count = 0; 411 | for prop in properties { 412 | match prop { 413 | Prop::B(_) => { 414 | move_seen = true; 415 | if identifiers.contains("W") { 416 | return Err(InvalidNodeError::MultipleMoves(format!( 417 | "{:?}", 418 | properties.to_vec() 419 | ))); 420 | } 421 | } 422 | Prop::W(_) => { 423 | move_seen = true; 424 | if identifiers.contains("B") { 425 | return Err(InvalidNodeError::MultipleMoves(format!( 426 | "{:?}", 427 | properties.to_vec() 428 | ))); 429 | } 430 | } 431 | Prop::CR(ps) | Prop::MA(ps) | Prop::SL(ps) | Prop::SQ(ps) | Prop::TR(ps) => { 432 | for p in ps.iter() { 433 | if markup_points.contains(&p) { 434 | return Err(InvalidNodeError::RepeatedMarkup(format!( 435 | "{:?}", 436 | properties.to_vec() 437 | ))); 438 | } 439 | markup_points.insert(p); 440 | } 441 | } 442 | Prop::DM(_) | Prop::UC(_) | Prop::GW(_) | Prop::GB(_) => { 443 | exclusive_node_annotations += 1 444 | } 445 | Prop::BM(_) | Prop::DO | Prop::IT | Prop::TE(_) => move_annotation_count += 1, 446 | Prop::Invalid(identifier, values) => { 447 | return Err(InvalidNodeError::InvalidProperty(format!( 448 | "{}, {:?}", 449 | identifier, values 450 | ))) 451 | } 452 | _ => {} 453 | } 454 | match prop.property_type() { 455 | Some(PropertyType::Move) => move_node = true, 456 | Some(PropertyType::Setup) => setup_node = true, 457 | Some(PropertyType::Root) => { 458 | if !is_root { 459 | return Err(InvalidNodeError::UnexpectedRootProperties(format!( 460 | "{:?}", 461 | properties 462 | ))); 463 | } 464 | } 465 | _ => {} 466 | } 467 | let ident = prop.identifier(); 468 | if identifiers.contains(&ident) { 469 | return Err(InvalidNodeError::RepeatedIdentifier(format!( 470 | "{:?}", 471 | properties.to_vec() 472 | ))); 473 | } 474 | identifiers.insert(prop.identifier()); 475 | } 476 | if setup_node && move_node { 477 | return Err(InvalidNodeError::SetupAndMove(format!( 478 | "{:?}", 479 | properties.to_vec() 480 | ))); 481 | } 482 | if identifiers.contains("KO") && !(identifiers.contains("B") || identifiers.contains("W")) { 483 | return Err(InvalidNodeError::KoWithoutMove(format!( 484 | "{:?}", 485 | properties.to_vec() 486 | ))); 487 | } 488 | if move_annotation_count > 1 { 489 | return Err(InvalidNodeError::MultipleMoveAnnotations(format!( 490 | "{:?}", 491 | properties.to_vec() 492 | ))); 493 | } 494 | if move_annotation_count == 1 && !move_seen { 495 | return Err(InvalidNodeError::UnexpectedMoveAnnotation(format!( 496 | "{:?}", 497 | properties.to_vec() 498 | ))); 499 | } 500 | if exclusive_node_annotations > 1 { 501 | return Err(InvalidNodeError::MultipleExclusiveAnnotations(format!( 502 | "{:?}", 503 | properties.to_vec() 504 | ))); 505 | } 506 | Ok(()) 507 | } 508 | } 509 | 510 | impl Eq for $name {} 511 | 512 | fn parse_size(values: &[String]) -> Result<(u8, u8), SgfPropError> { 513 | if values.len() != 1 { 514 | return Err(SgfPropError {}); 515 | } 516 | let value = &values[0]; 517 | if value.contains(':') { 518 | crate::props::parse::parse_tuple(value) 519 | } else { 520 | let size = value.parse().map_err(|_| SgfPropError {})?; 521 | Ok((size, size)) 522 | } 523 | } 524 | 525 | fn parse_labels( 526 | values: &[String], 527 | ) -> Result, SgfPropError> { 528 | let mut labels = HashSet::new(); 529 | for value in values.iter() { 530 | let (s1, s2) = crate::props::parse::split_compose(value)?; 531 | labels.insert(( 532 | s1.parse().map_err(|_| SgfPropError {})?, 533 | crate::SimpleText { 534 | text: s2.to_string(), 535 | }, 536 | )); 537 | } 538 | if labels.is_empty() { 539 | return Err(SgfPropError {}); 540 | } 541 | 542 | Ok(labels) 543 | } 544 | 545 | fn parse_figure(values: &[String]) -> Result, SgfPropError> { 546 | if values.is_empty() || (values.len() == 1 && values[0].is_empty()) { 547 | return Ok(None); 548 | } 549 | if values.len() > 1 { 550 | return Err(SgfPropError {}); 551 | } 552 | let (s1, s2) = crate::props::parse::split_compose(&values[0])?; 553 | 554 | Ok(Some(( 555 | s1.parse().map_err(|_| SgfPropError {})?, 556 | crate::SimpleText { 557 | text: s2.to_string(), 558 | }, 559 | ))) 560 | } 561 | 562 | fn parse_application(values: &[String]) -> Result<(crate::SimpleText, crate::SimpleText), SgfPropError> { 563 | if values.len() != 1 { 564 | return Err(SgfPropError {}); 565 | } 566 | let (s1, s2) = crate::props::parse::split_compose(&values[0])?; 567 | Ok(( 568 | crate::SimpleText { 569 | text: s1.to_string(), 570 | }, 571 | crate::SimpleText { 572 | text: s2.to_string(), 573 | }, 574 | )) 575 | } 576 | } 577 | } 578 | --------------------------------------------------------------------------------