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;
|
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
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 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
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