cuna/
parser.rs

1use crate::error::Error;
2use crate::error::InvalidArgument;
3use crate::error::ParseError;
4use crate::time::TimeStamp;
5use crate::track::Disc;
6use crate::track::Index;
7use crate::track::Track;
8use crate::utils;
9use crate::Cuna;
10use std::fmt;
11use std::iter::Enumerate;
12use std::str::Lines;
13
14pub type Parser<'a> = Parna<Enumerate<Lines<'a>>>;
15
16macro_rules! fail {
17    (token $token: expr) => {
18        return Err($crate::error::ParseError::unexpected_token($token))
19    };
20    (syntax $cmd: expr, $msg: expr) => {
21        return Err($crate::error::ParseError::syntax_error($cmd, $msg))
22    }
23}
24
25#[derive(Debug, Clone, PartialEq, Eq, Hash)]
26pub enum Command<'a> {
27    Rem(&'a str),
28    Title(&'a str),
29    Performer(&'a str),
30    Songwriter(&'a str),
31    Catalog(u64),
32    Cdtextfile(&'a str),
33    File(&'a str, &'a str),
34    Track(u8, &'a str),
35    Index(u8, TimeStamp),
36    Pregap(&'a str),
37    Postgap(&'a str),
38    Isrc(&'a str),
39    Flags(&'a str),
40    Empty,
41}
42
43/// A lazy parser takes iterators of `(usize, &str)`
44/// which won't parse anything unless `Parna::parse*()` is called
45///
46/// In most cases, it is constructed with an str using [`Parna::new()`](Parna::new)
47///
48/// It only stores the original data
49/// and results will be written to `Cuna` which is passed to `Parna::parse*()`
50#[derive(Debug, Clone)]
51pub struct Parna<I>(I);
52
53impl<'a> Command<'a> {
54    pub fn new(s: &'a str) -> Result<Self, ParseError> {
55        let s = match s.trim() {
56            "" => return Ok(Self::Empty),
57            ts => ts,
58        };
59        let (content, command) = match utils::token(s) {
60            Ok(ok) => ok,
61            Err(_) => fail!(syntax s, "missing arguments"),
62        };
63        match command.to_ascii_lowercase().as_ref() {
64            "rem" => Ok(Self::Rem(content)),
65            "title" => Ok(Self::Title(trimq(content))),
66            "performer" => Ok(Self::Performer(trimq(content))),
67            "songwriter" => Ok(Self::Songwriter(trimq(content))),
68            "catalog" => match utils::number(13)(content) {
69                Ok((_, catalog)) => Ok(Self::Catalog(catalog)),
70                Err(_) => fail!(syntax content, "invaild catalog"),
71            },
72            "cdtextfile" => Ok(Self::Cdtextfile(trimq(content))),
73            "file" => match utils::quote_opt(content) {
74                Ok(("", _)) | Err(_) => Err(InvalidArgument::MissingArgument.into()),
75                Ok((format, path)) => Ok(Self::File(trimq(path), format.trim())),
76            },
77            "track" => match utils::token(content) {
78                Ok((format, id)) => Ok(Self::Track(parse_id(id)?, format)),
79                Err(_) => Err(InvalidArgument::MissingArgument.into()),
80            },
81            "index" => match utils::token(content) {
82                Ok((timestamp, id)) => Ok(Self::Index(parse_id(id)?, timestamp.parse()?)),
83                Err(_) => Err(InvalidArgument::MissingArgument.into()),
84            },
85            "pregap" => Ok(Self::Pregap(trimq(content))),
86            "postgap" => Ok(Self::Postgap(trimq(content))),
87            "isrc" => Ok(Self::Isrc(trimq(content))),
88            "flags" => Ok(Self::Flags(trimq(content))),
89            _ => Err(ParseError::unexpected_token(command)),
90        }
91    }
92    pub fn parse(&self, sheet: &mut Cuna) -> Result<(), ParseError> {
93        match *self {
94            Self::Empty => {}
95            Self::Rem(s) => sheet.comments.push(s.to_owned()),
96            Self::Title(s) => match sheet.last_track_mut() {
97                Some(tk) => tk.push_title(s.to_owned()),
98                None => sheet.header.push_title(s.to_owned()),
99            },
100            Self::Performer(s) => match sheet.last_track_mut() {
101                Some(tk) => tk.push_performer(s.to_owned()),
102                _ => sheet.header.push_performer(s.to_owned()),
103            },
104            Self::Songwriter(s) => match sheet.last_track_mut() {
105                Some(tk) => tk.push_songwriter(s.to_owned()),
106                _ => sheet.header.push_songwriter(s.to_owned()),
107            },
108            Self::Catalog(s) => match sheet.header.catalog {
109                None => sheet.header.catalog = Some(s),
110                _ => fail!(syntax self, "multiple `CATALOG` commands is not allowed"),
111            },
112            Self::Cdtextfile(s) => {
113                sheet.header.set_cdtextfile(s.to_owned());
114            }
115            Self::File(name, format) => {
116                sheet.push_file(Disc::new(name.to_owned(), format.to_owned()));
117            }
118            Self::Track(id, format) => match sheet.last_file_mut() {
119                Some(tk) => tk.push_track(Track::new_unchecked(id, format.to_owned())),
120                None => fail!(token "TRACK"),
121            },
122            Self::Index(id, timestamp) => match sheet.last_track_mut() {
123                Some(tk) if tk.postgap.is_none() => {
124                    tk.push_index(Index::new_unchecked(id, timestamp))
125                }
126                Some(_) => fail!(syntax self, "Command `INDEX` should be before `POSTGAP`"),
127                None => fail!(token "INDEX"),
128            },
129            Self::Pregap(timestamp) => match sheet.last_track_mut() {
130                Some(tk) if tk.index.is_empty() && tk.pregap.is_none() => {
131                    tk.set_pregep(timestamp.parse()?);
132                }
133                Some(tk) if tk.pregap.is_some() => {
134                    fail!(syntax self, "Multiple `PREGAP` commands are not allowed in one `TRACK` scope")
135                }
136                Some(_) => fail!(syntax self, "Command `PREGAP` should be before `INDEX`"),
137                _ => fail!(token "PREGAP"),
138            },
139            Self::Postgap(timestamp) => match sheet.last_track_mut() {
140                Some(tk) if tk.postgap.is_none() => {
141                    tk.set_postgep(timestamp.parse()?);
142                }
143                Some(_) => {
144                    fail!(syntax self, "Multiple `POSTGAP` commands are not allowed in one `TRACK` scope")
145                }
146                None => fail!(token "POSTGAP"),
147            },
148            Self::Isrc(s) => match sheet.last_track_mut() {
149                Some(tk) if tk.isrc.is_none() => {
150                    tk.set_isrc(s.to_owned());
151                }
152                Some(_) => {
153                    fail!(syntax self, "Multiple `ISRC` commands are not allowed in one `TRACK` scope")
154                }
155                None => fail!(token "ISRC"),
156            },
157            Self::Flags(s) => match sheet.last_track_mut() {
158                Some(tk) if tk.flags.is_empty() => tk.push_flags(s.split(' ')),
159                Some(_) => {
160                    fail!(syntax self, "Multiple `FLAGS` commands are not allowed in one `TRACK` scope")
161                }
162                None => fail!(token "FLAGS"),
163            },
164        }
165        Ok(())
166    }
167}
168impl fmt::Display for Command<'_> {
169    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
170        match *self {
171            Self::Rem(c) => write!(formatter, "REM {}", c),
172            Self::Title(c) => write!(formatter, r#"TITLE "{}""#, c),
173            Self::Performer(c) => write!(formatter, r#"PERFORMER "{}""#, c),
174            Self::Songwriter(c) => write!(formatter, r#"SONGWRITER "{}""#, c),
175            Self::Catalog(c) => write!(formatter, "CATALOG {}", c),
176            Self::Cdtextfile(c) => write!(formatter, r#"CDTEXTFILE "{}""#, c),
177            Self::File(name, tp) => write!(formatter, r#"FILE "{}" {}"#, name, tp),
178            Self::Track(id, format) => write!(formatter, "TRACK {} {}", id, format),
179            Self::Index(id, timestamp) => write!(formatter, "INDEX {} {}", id, timestamp),
180            Self::Pregap(c) => write!(formatter, "PREGAP {}", c),
181            Self::Postgap(c) => write!(formatter, "POSTGAP {}", c),
182            Self::Isrc(c) => write!(formatter, "ISRC {}", c),
183            Self::Flags(c) => write!(formatter, "FLAG {}", c),
184            Self::Empty => Ok(()),
185        }
186    }
187}
188impl<'a> Parna<Enumerate<Lines<'a>>> {
189    /// Returns a new Parser
190    pub fn new(s: &'a str) -> Self {
191        Self(s.lines().enumerate())
192    }
193}
194impl<'a, I: Iterator<Item = &'a str>> Parna<Enumerate<I>> {
195    pub fn from_lines(lines: I) -> Self {
196        Self(lines.enumerate())
197    }
198    #[deprecated]
199    pub fn set_lines(&mut self, lines: I) {
200        self.0 = lines.enumerate();
201    }
202}
203impl<'a, I: Iterator<Item = (usize, &'a str)>> Parna<I> {
204    /// Constructs a `Parna` with a generic iterator of `(usize, &str)`
205    ///
206    /// The `usize` represents which line is being parsed
207    /// and the `&str` represents the actual data
208    pub fn with_iter(it: I) -> Self {
209        Self(it)
210    }
211    /// Returns a mut reference to the internal iterator
212    pub fn data(&mut self) -> &mut I {
213        self.0.by_ref()
214    }
215    /// Parses one line and writes to `state`
216    pub fn parse_next_line(&mut self, state: &mut Cuna) -> Result<(), Error> {
217        self.parse_next_n_lines(1, state)
218    }
219    /// Parses n lines and writes to `state`
220    ///
221    /// Each line will be parsed and written to `state` until an `Error` is returned
222    pub fn parse_next_n_lines(&mut self, n: usize, state: &mut Cuna) -> Result<(), Error> {
223        for (at, line) in self.0.by_ref().take(n) {
224            let to_error = |e| Error::new(e, at + 1);
225            Command::new(line)
226                .map_err(to_error)?
227                .parse(state)
228                .map_err(to_error)?;
229        }
230        Ok(())
231    }
232    /// Parses all the lines and writes to `state`
233    ///
234    /// Each line will be parsed and written to `state` until an `Error` is returned
235    ///
236    /// If all the lines are parsed successfully, an `Ok(())` will be returned
237    pub fn parse(&mut self, state: &mut Cuna) -> Result<(), Error> {
238        for (at, line) in self.0.by_ref() {
239            let to_error = |e| Error::new(e, at + 1);
240            Command::new(line)
241                .map_err(to_error)?
242                .parse(state)
243                .map_err(to_error)?;
244        }
245        Ok(())
246    }
247}
248impl<'a, I: Iterator<Item = (usize, &'a str)> + Clone> Parna<I> {
249    /// Returns the current line to be parsed
250    /// ```rust
251    /// use cuna::parser::Parser;
252    /// let line = r#"TITLE "HELLO WORLD オリジナル・サウンドトラック""#;
253    /// let parser = Parser::new(line);
254    /// assert_eq!(parser.current_line(), Some(line));
255    /// ```
256    pub fn current_line(&self) -> Option<&'a str> {
257        self.current().map(|(_, s)| s)
258    }
259    /// Like [`current_line()`](Parna::current_line), but returns line number at the same time
260    pub fn current(&self) -> Option<(usize, &'a str)> {
261        self.0.clone().next()
262    }
263}
264
265#[inline(always)]
266fn parse_id(s: &str) -> Result<u8, InvalidArgument> {
267    Ok(utils::number(2)(s)
268        .map_err(|_| InvalidArgument::InvalidId)?
269        .1)
270}
271#[inline(always)]
272fn trimq(s: &str) -> &str {
273    s.trim_matches('"')
274}