Play around with line-wise parsing

This commit is contained in:
Joscha 2021-11-17 23:48:09 +01:00
parent b89ed3e2df
commit 9c9e5764f2
4 changed files with 138 additions and 11 deletions

View file

@ -5,7 +5,9 @@ use crate::commands::Command;
use self::line::parse_lines; use self::line::parse_lines;
mod error;
mod line; mod line;
mod parser;
pub fn parse(file: &Path) -> anyhow::Result<Vec<Command>> { pub fn parse(file: &Path) -> anyhow::Result<Vec<Command>> {
let content = fs::read_to_string(file)?; let content = fs::read_to_string(file)?;

32
src/parse/error.rs Normal file
View file

@ -0,0 +1,32 @@
use std::error;
#[derive(Debug, thiserror::Error)]
#[error("line {line}: {reason}")]
pub struct ParseError {
line: usize,
reason: Box<dyn error::Error>,
}
impl ParseError {
#[must_use]
pub fn new(line: usize, reason: impl error::Error + 'static) -> Self {
Self {
line,
reason: Box::new(reason),
}
}
#[must_use]
pub fn pack<T>(line: usize, reason: impl error::Error + 'static) -> Result<T, Self> {
Err(Self::new(line, reason))
}
}
pub trait ToParseError: error::Error + 'static + Sized {
#[must_use]
fn at(self, line: usize) -> ParseError {
ParseError::new(line, self)
}
}
impl<E: error::Error + 'static> ToParseError for E {}

View file

@ -4,6 +4,8 @@ use chrono::NaiveDate;
use crate::commands::{BirthdaySpec, Done, Spec}; use crate::commands::{BirthdaySpec, Done, Spec};
use super::error::ParseError;
#[derive(Debug)] #[derive(Debug)]
pub enum Line { pub enum Line {
Empty, Empty,
@ -21,14 +23,16 @@ pub enum Line {
} }
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum Error { pub enum Reason {
#[error("line {line}: unknown command {name:?}")] #[error("unknown format")]
UnknownCommand { line: usize, name: String }, UnknownFormat,
#[error("line {line}: unknown format")] #[error("unknown command {0:?}")]
UnknownFormat { line: usize }, UnknownCommand(String),
#[error("empty command body")]
EmptyCommand,
} }
type Result<T> = result::Result<T, Error>; type Result<T> = result::Result<T, ParseError>;
pub fn parse_lines(content: &str) -> Result<Vec<Line>> { pub fn parse_lines(content: &str) -> Result<Vec<Line>> {
content content
@ -46,6 +50,10 @@ fn parse_line(line: usize, content: &str) -> Result<Line> {
} else if content.starts_with('\t') || content.starts_with(' ') { } else if content.starts_with('\t') || content.starts_with(' ') {
Ok(Line::Indented(content.to_string())) Ok(Line::Indented(content.to_string()))
} else if let Some((name, rest)) = parse_command(content) { } else if let Some((name, rest)) = parse_command(content) {
let rest = rest.trim();
if rest.is_empty() {
return ParseError::pack(line, Reason::EmptyCommand);
}
match name { match name {
"TASK" => Ok(Line::Task(rest.to_string())), "TASK" => Ok(Line::Task(rest.to_string())),
"NOTE" => Ok(Line::Note(rest.to_string())), "NOTE" => Ok(Line::Note(rest.to_string())),
@ -56,13 +64,10 @@ fn parse_line(line: usize, content: &str) -> Result<Line> {
"UNTIL" => parse_datum(rest).map(Line::Until), "UNTIL" => parse_datum(rest).map(Line::Until),
"EXCEPT" => parse_datum(rest).map(Line::Except), "EXCEPT" => parse_datum(rest).map(Line::Except),
"DONE" => parse_done(rest), "DONE" => parse_done(rest),
_ => Err(Error::UnknownCommand { _ => ParseError::pack(line, Reason::UnknownCommand(name.to_string())),
line,
name: name.to_string(),
}),
} }
} else { } else {
Err(Error::UnknownFormat { line }) ParseError::pack(line, Reason::UnknownFormat)
} }
} }

88
src/parse/parser.rs Normal file
View file

@ -0,0 +1,88 @@
pub struct Parser<'d> {
data: &'d str,
index: usize,
}
#[derive(Debug, thiserror::Error)]
pub enum Reason {
#[error("expected character {expected:?} at {rest:?}")]
ExpectedChar { expected: char, rest: String },
#[error("expected string {expected:?} at {rest:?}")]
ExpectedStr { expected: String, rest: String },
#[error("expected whitespace at {rest:?}")]
ExpectedWhitespace { rest: String },
}
impl<'d> Parser<'d> {
pub fn new(data: &'d str) -> Self {
Self { data, index: 0 }
}
fn rest(&self) -> &'d str {
&self.data[self.index..]
}
pub fn peek(&self) -> Option<char> {
self.rest().chars().next()
}
pub fn take(&mut self) -> Option<char> {
if let Some(c) = self.peek() {
self.index += c.len_utf8();
Some(c)
} else {
None
}
}
pub fn take_exact(&mut self, c: char) -> Result<(), Reason> {
if self.peek() == Some(c) {
self.take();
Ok(())
} else {
Err(Reason::ExpectedChar {
expected: c,
rest: self.rest().to_string(),
})
}
}
pub fn take_any_whitespace(&mut self) {
while let Some(c) = self.peek() {
if c.is_whitespace() {
self.take();
} else {
break;
}
}
}
pub fn take_some_whitespace(&mut self) -> Result<(), Reason> {
match self.peek() {
Some(c) if c.is_whitespace() => {
self.take();
self.take_any_whitespace();
Ok(())
}
_ => Err(Reason::ExpectedWhitespace {
rest: self.rest().to_string(),
}),
}
}
pub fn starts_with(&self, pattern: &str) -> bool {
self.data.starts_with(pattern)
}
pub fn take_starting_with(&mut self, pattern: &str) -> Result<(), Reason> {
if self.starts_with(pattern) {
self.index += pattern.len();
Ok(())
} else {
Err(Reason::ExpectedStr {
expected: pattern.to_string(),
rest: self.rest().to_string(),
})
}
}
}