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