1use crate::game::{Command, Nag, Outcome, Tag as OtherTag, Text};
2
3use super::*;
4use crate::formats::{StrInput as Input, fen, prelude::*, san};
5
6#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
7#[error("PGN error at line {line}, column {column}: {message}")]
8pub struct Error {
9 pub line: usize,
10 pub column: usize,
11 pub message: String,
12}
13
14pub type Result<T, E = Error> = core::result::Result<T, E>;
15
16pub fn game(input: &mut Input<'_>) -> ModalResult<Game> {
17 delimited(multispace0, (tags, comments, repeat(0.., parse_move), outcome), multispace0)
18 .map(|(tags, intro, moves, outcome)| Game {
19 start: start_position(&tags),
20 tags,
21 intro,
22 moves,
23 outcome,
24 })
25 .context(StrContext::Label("PGN game"))
26 .parse_next(input)
27}
28
29impl Error {
30 pub fn from(
31 text: &str,
32 first_line: usize,
33 error: winnow::error::ParseError<Input<'_>, ContextError>,
34 ) -> Self {
35 let (line, column) = line_column(text, first_line, error.offset());
36 Self { line, column, message: format!("{:?}", error.inner()) }
37 }
38}
39
40fn line_column(input: &str, first_line: usize, offset: usize) -> (usize, usize) {
41 let mut line = first_line;
42 let mut column = 1;
43
44 for (index, char) in input.char_indices() {
45 if index >= offset {
46 break;
47 }
48 if char == '\n' {
49 line += 1;
50 column = 1;
51 } else {
52 column += 1;
53 }
54 }
55
56 (line, column)
57}
58
59pub fn strip_tags(mut input: &str) -> &str {
60 loop {
61 let before = input;
62 if tag.parse_next(&mut input).is_err() {
63 return before;
64 }
65 }
66}
67
68pub fn tag_start_ok(mut input: &str) -> bool {
69 tag.parse_next(&mut input).is_ok()
70}
71
72pub fn tags(input: &mut Input<'_>) -> ModalResult<Vec<Tag>> {
73 repeat(0.., tag).parse_next(input)
74}
75
76pub fn tag(input: &mut Input<'_>) -> ModalResult<Tag> {
77 delimited(
78 multispace0,
79 delimited(
80 ('[', multispace0),
81 separated_pair(name, multispace1, tag_value),
82 (multispace0, ']'),
83 ),
84 multispace0,
85 )
86 .verify_map(|(key, value)| tag_from_pair(key, value))
87 .context(StrContext::Label("PGN tag pair"))
88 .parse_next(input)
89}
90
91pub fn parse_move(input: &mut Input<'_>) -> ModalResult<Move> {
92 preceded(
93 (multispace0, opt(skip_move_number), multispace0),
94 (san::san.context(StrContext::Label("PGN move")), tail, variations).map(
95 |(san, (comment, annotations), variations)| Move {
96 san,
97 comment,
98 annotations,
99 variations,
100 },
101 ),
102 )
103 .parse_next(input)
104}
105
106pub fn variations(input: &mut Input<'_>) -> ModalResult<Vec<Variation>> {
107 repeat(0.., preceded(multispace0, variation)).parse_next(input)
108}
109
110pub fn variation(input: &mut Input<'_>) -> ModalResult<Variation> {
112 let mut variation = delimited(
113 ('(', multispace0),
114 seq! {Variation {
115 intro: comments,
116 moves: repeat(0.., parse_move),
117 outro: ().value(None),
118 }},
119 (multispace0, ')'),
120 )
121 .context(StrContext::Label("PGN variation"))
122 .parse_next(input)?;
123 variation.outro = comments(input)?;
124 Ok(variation)
125}
126
127fn tail(input: &mut Input<'_>) -> ModalResult<(Option<Comment>, Vec<Annotation>)> {
128 repeat(0.., preceded(multispace0, tail_item))
129 .fold(
130 || (None, Vec::new()),
131 |(mut comment, mut annotations), item| {
132 match item {
133 Tail::Comment(MoveComment { commands, text }) => {
134 annotations.extend(commands.into_iter().map(Annotation::Command));
135 if let Some(text) = text {
136 merge_comments(&mut comment, text);
137 }
138 }
139 Tail::Annotation(annotation) => annotations.push(annotation),
140 }
141 (comment, annotations)
142 },
143 )
144 .parse_next(input)
145}
146
147enum Tail {
148 Comment(MoveComment),
149 Annotation(Annotation),
150}
151
152fn tail_item(input: &mut Input<'_>) -> ModalResult<Tail> {
153 alt((
154 move_comment.map(Tail::Comment),
155 numeric_nag.map(Annotation::Nag).map(Tail::Annotation),
156 symbol_nag.map(Annotation::Nag).map(Tail::Annotation),
157 ))
158 .parse_next(input)
159}
160
161fn comments(input: &mut Input<'_>) -> ModalResult<Option<Comment>> {
162 repeat(0.., preceded(multispace0, comment))
163 .fold(
164 || None,
165 |mut comments, comment| {
166 if let Some(comment) = comment {
167 merge_comments(&mut comments, comment);
168 }
169 comments
170 },
171 )
172 .parse_next(input)
173}
174
175fn skip_move_number(input: &mut Input<'_>) -> ModalResult<()> {
177 (dec_uint::<_, u32, _>, alt(("...", "."))).value(()).parse_next(input)
178}
179
180fn tag_from_pair(key: Text, value: String) -> Option<Tag> {
181 Some(match key.as_ref() {
182 "Event" => Tag::Event(value),
183 "Site" => Tag::Site(value),
184 "Date" => Tag::Date(value),
185 "Round" => Tag::Round(value),
186 "White" => Tag::White(value),
187 "Black" => Tag::Black(value),
188 "Result" => Tag::Outcome(outcome.parse(value.as_str()).ok()?),
189 "FEN" => Tag::Fen(fen::parse_position.parse(value.as_str()).ok()?),
190 "SetUp" => Tag::SetUp(match value.as_str() {
191 "0" => false,
192 "1" => true,
193 _ => return None,
194 }),
195 "Variant" => Tag::Variant(value),
196 _ => Tag::Other(OtherTag { key, value }),
197 })
198}
199
200fn tag_value(input: &mut Input<'_>) -> ModalResult<String> {
201 delimited(
202 '"',
203 repeat(0.., tag_value_char).fold(String::new, |mut value, c| {
204 value.push(c);
205 value
206 }),
207 '"',
208 )
209 .parse_next(input)
210}
211
212fn tag_value_char(input: &mut Input<'_>) -> ModalResult<char> {
213 alt((preceded('\\', any), none_of(['"', '\\']))).parse_next(input)
214}
215
216fn name(input: &mut Input<'_>) -> ModalResult<Text> {
217 take_while(1.., |c: char| c.is_ascii_alphanumeric() || c == '_')
218 .verify_map(Text::new)
219 .parse_next(input)
220}
221
222fn numeric_nag(input: &mut Input<'_>) -> ModalResult<Nag> {
223 preceded('$', dec_uint).map(Nag::Numeric).parse_next(input)
224}
225
226fn symbol_nag(input: &mut Input<'_>) -> ModalResult<Nag> {
227 alt(("!!", "!?", "?!", "??", "!", "?"))
228 .map(|nag: &str| Nag::Symbol(nag.to_string()))
229 .parse_next(input)
230}
231
232pub fn comment(input: &mut Input<'_>) -> ModalResult<Option<Comment>> {
233 alt((bracket_comment, semicolon_comment))
234 .map(|comment| Text::new(comment).map(Comment))
235 .context(StrContext::Label("PGN comment"))
236 .parse_next(input)
237}
238
239fn move_comment(input: &mut Input<'_>) -> ModalResult<MoveComment> {
240 alt((bracket_comment, semicolon_comment))
241 .map(split_comment)
242 .context(StrContext::Label("PGN comment"))
243 .parse_next(input)
244}
245
246fn bracket_comment(input: &mut Input<'_>) -> ModalResult<String> {
247 preceded('{', terminated(take_till(0.., '}'), '}')).map(ToString::to_string).parse_next(input)
248}
249
250fn semicolon_comment(input: &mut Input<'_>) -> ModalResult<String> {
251 preceded(';', take_till(0.., '\n')).map(ToString::to_string).parse_next(input)
252}
253
254fn split_comment(raw: String) -> MoveComment {
255 let mut commands = Vec::new();
256 let mut comment = String::new();
257 let mut rest: Input<'_> = raw.as_str();
258
259 while let Some(start) = rest.find("[%") {
260 comment.push_str(&rest[..start]);
261 rest = &rest[start..];
262
263 let mut candidate: Input<'_> = rest;
264 if let Ok(command) = command.parse_next(&mut candidate) {
265 commands.push(command);
266 rest = candidate;
267 } else {
268 if let Some((invalid_command, next)) = rest.split_once(']') {
269 comment.push_str(invalid_command);
270 comment.push(']');
271 rest = next;
272 } else {
273 comment.push_str(rest);
274 rest = "";
275 }
276 }
277 }
278
279 comment.push_str(rest);
280 MoveComment { commands, text: Text::new(comment).map(Comment) }
281}
282
283fn command(input: &mut Input<'_>) -> ModalResult<Command> {
284 delimited("[%", (name, opt(preceded(space1, parameters))), (space0, ']'))
285 .map(|(command, parameters): (Text, Option<Vec<String>>)| Command {
286 command,
287 parameters: parameters.unwrap_or_default(),
288 })
289 .parse_next(input)
290}
291
292fn parameters(input: &mut Input<'_>) -> ModalResult<Vec<String>> {
293 separated(0.., parameter, ',').parse_next(input)
294}
295
296fn parameter(input: &mut Input<'_>) -> ModalResult<String> {
297 alt((quoted_parameter, unquoted_parameter)).parse_next(input)
298}
299
300fn quoted_parameter(input: &mut Input<'_>) -> ModalResult<String> {
301 delimited('"', take_till(0.., '"'), '"').take().map(ToString::to_string).parse_next(input)
304}
305
306fn unquoted_parameter(input: &mut Input<'_>) -> ModalResult<String> {
307 take_till(1.., [',', ']']).map(|parameter: &str| parameter.trim().to_string()).parse_next(input)
308}
309
310fn merge_comments(into: &mut Option<Comment>, comment: Comment) {
311 match into {
312 Some(into) => into.merge(&comment),
313 None => *into = Some(comment),
314 }
315}
316
317fn outcome(input: &mut Input<'_>) -> ModalResult<Outcome> {
318 use Outcome::*;
319
320 preceded(
321 multispace0,
322 alt(("1/2-1/2".value(Draw), "1-0".value(White), "0-1".value(Black), "*".value(Unknown))),
323 )
324 .context(StrContext::Label("PGN outcome"))
325 .parse_next(input)
326}