sgf_parse/props/
values.rs

1use std::str::FromStr;
2
3use super::SgfPropError;
4
5/// An SGF [Color](https://www.red-bean.com/sgf/sgf4.html#types) value.
6#[derive(Copy, Clone, Debug, Eq, PartialEq)]
7pub 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)]
14pub 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)]
32pub 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)]
48pub 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)]
54pub enum PropertyType {
55    Move,
56    Setup,
57    Root,
58    GameInfo,
59    Inherit,
60}
61
62impl FromStr for Double {
63    type Err = SgfPropError;
64
65    fn from_str(s: &str) -> Result<Self, Self::Err> {
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
76impl FromStr for Color {
77    type Err = SgfPropError;
78
79    fn from_str(s: &str) -> Result<Self, Self::Err> {
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
90impl std::convert::From<&str> for SimpleText {
91    fn from(s: &str) -> Self {
92        Self { text: s.to_owned() }
93    }
94}
95
96impl std::convert::From<&str> for Text {
97    fn from(s: &str) -> Self {
98        Self { text: s.to_owned() }
99    }
100}
101
102impl FromStr for SimpleText {
103    type Err = SgfPropError;
104
105    fn from_str(s: &str) -> Result<Self, Self::Err> {
106        Ok(s.into())
107    }
108}
109
110impl FromStr for Text {
111    type Err = SgfPropError;
112
113    fn from_str(s: &str) -> Result<Self, Self::Err> {
114        Ok(s.into())
115    }
116}
117
118impl 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
128impl 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
135fn 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<char> = 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)]
178mod 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}