Play around with line-wise parsing
This commit is contained in:
parent
b89ed3e2df
commit
9c9e5764f2
4 changed files with 138 additions and 11 deletions
|
|
@ -5,7 +5,9 @@ use crate::commands::Command;
|
|||
|
||||
use self::line::parse_lines;
|
||||
|
||||
mod error;
|
||||
mod line;
|
||||
mod parser;
|
||||
|
||||
pub fn parse(file: &Path) -> anyhow::Result<Vec<Command>> {
|
||||
let content = fs::read_to_string(file)?;
|
||||
|
|
|
|||
32
src/parse/error.rs
Normal file
32
src/parse/error.rs
Normal 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 {}
|
||||
|
|
@ -4,6 +4,8 @@ use chrono::NaiveDate;
|
|||
|
||||
use crate::commands::{BirthdaySpec, Done, Spec};
|
||||
|
||||
use super::error::ParseError;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Line {
|
||||
Empty,
|
||||
|
|
@ -21,14 +23,16 @@ pub enum Line {
|
|||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("line {line}: unknown command {name:?}")]
|
||||
UnknownCommand { line: usize, name: String },
|
||||
#[error("line {line}: unknown format")]
|
||||
UnknownFormat { line: usize },
|
||||
pub enum Reason {
|
||||
#[error("unknown format")]
|
||||
UnknownFormat,
|
||||
#[error("unknown command {0:?}")]
|
||||
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>> {
|
||||
content
|
||||
|
|
@ -46,6 +50,10 @@ fn parse_line(line: usize, content: &str) -> Result<Line> {
|
|||
} else if content.starts_with('\t') || content.starts_with(' ') {
|
||||
Ok(Line::Indented(content.to_string()))
|
||||
} 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 {
|
||||
"TASK" => Ok(Line::Task(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),
|
||||
"EXCEPT" => parse_datum(rest).map(Line::Except),
|
||||
"DONE" => parse_done(rest),
|
||||
_ => Err(Error::UnknownCommand {
|
||||
line,
|
||||
name: name.to_string(),
|
||||
}),
|
||||
_ => ParseError::pack(line, Reason::UnknownCommand(name.to_string())),
|
||||
}
|
||||
} else {
|
||||
Err(Error::UnknownFormat { line })
|
||||
ParseError::pack(line, Reason::UnknownFormat)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
88
src/parse/parser.rs
Normal file
88
src/parse/parser.rs
Normal 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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue