Implement includes and setting a time zone
This commit is contained in:
parent
b4b8dd53e0
commit
36aa6abcb9
3 changed files with 105 additions and 28 deletions
47
src/files.rs
47
src/files.rs
|
|
@ -19,9 +19,16 @@ pub struct Files {
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error("{0}")]
|
#[error("{0}")]
|
||||||
IoError(#[from] io::Error),
|
Io(#[from] io::Error),
|
||||||
#[error("{0}")]
|
#[error("{0}")]
|
||||||
ParseError(#[from] parse::Error),
|
Parse(#[from] parse::Error),
|
||||||
|
#[error("{file1} has time zone {tz1} but {file2} has time zone {tz2}")]
|
||||||
|
TzConflict {
|
||||||
|
file1: PathBuf,
|
||||||
|
tz1: Tz,
|
||||||
|
file2: PathBuf,
|
||||||
|
tz2: Tz,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Result<T> = result::Result<T, Error>;
|
pub type Result<T> = result::Result<T, Error>;
|
||||||
|
|
@ -33,30 +40,56 @@ impl Files {
|
||||||
timezone: None,
|
timezone: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
new.load_file(path.to_owned())?;
|
new.load_file(path)?;
|
||||||
new.determine_timezone()?;
|
new.determine_timezone()?;
|
||||||
|
|
||||||
Ok(new)
|
Ok(new)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_file(&mut self, path: PathBuf) -> Result<()> {
|
fn load_file(&mut self, path: &Path) -> Result<()> {
|
||||||
let canon_path = path.canonicalize()?;
|
let canon_path = path.canonicalize()?;
|
||||||
if self.files.contains_key(&canon_path) {
|
if self.files.contains_key(&canon_path) {
|
||||||
// We've already loaded this exact file.
|
// We've already loaded this exact file.
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let content = fs::read_to_string(&path)?;
|
let content = fs::read_to_string(path)?;
|
||||||
let file = parse::parse(path, &content)?;
|
let file = parse::parse(path, &content)?;
|
||||||
|
let includes = file.includes.clone();
|
||||||
|
|
||||||
self.files.insert(canon_path, file);
|
self.files.insert(canon_path, file);
|
||||||
|
|
||||||
// TODO Also load all included files
|
for include in includes {
|
||||||
|
self.load_file(&include)?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn determine_timezone(&mut self) -> Result<()> {
|
fn determine_timezone(&mut self) -> Result<()> {
|
||||||
// TODO Implement once files can specify time zones
|
let mut found: Option<(PathBuf, Tz)> = None;
|
||||||
|
|
||||||
|
for file in self.files.values() {
|
||||||
|
if let Some(file_tz) = file.timezone {
|
||||||
|
if let Some((found_name, found_tz)) = &found {
|
||||||
|
if *found_tz != file_tz {
|
||||||
|
return Err(Error::TzConflict {
|
||||||
|
file1: found_name.clone(),
|
||||||
|
tz1: *found_tz,
|
||||||
|
file2: file.name.clone(),
|
||||||
|
tz2: file_tz,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
found = Some((file.name.clone(), file_tz));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((_, tz)) = found {
|
||||||
|
self.timezone = Some(tz);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,9 @@ WHITESPACE = _{ !eol ~ WHITE_SPACE }
|
||||||
rest_some = { (!eol ~ ANY)+ }
|
rest_some = { (!eol ~ ANY)+ }
|
||||||
rest_any = { (!eol ~ ANY)* }
|
rest_any = { (!eol ~ ANY)* }
|
||||||
|
|
||||||
|
include = { "INCLUDE" ~ WHITESPACE ~ rest_some ~ eol }
|
||||||
|
timezone = { "TIMEZONE" ~ WHITESPACE ~ rest_some ~ eol }
|
||||||
|
|
||||||
number = @{ ASCII_DIGIT{1,9} } // Fits into an i32
|
number = @{ ASCII_DIGIT{1,9} } // Fits into an i32
|
||||||
|
|
||||||
title = { WHITESPACE ~ rest_some ~ eol }
|
title = { WHITESPACE ~ rest_some ~ eol }
|
||||||
|
|
@ -136,6 +139,6 @@ birthday = {
|
||||||
}
|
}
|
||||||
|
|
||||||
empty_line = _{ WHITESPACE* ~ NEWLINE }
|
empty_line = _{ WHITESPACE* ~ NEWLINE }
|
||||||
command = { task | note | birthday }
|
command = { include | timezone | task | note | birthday }
|
||||||
|
|
||||||
file = ${ SOI ~ (empty_line* ~ command)* ~ empty_line* ~ WHITESPACE* ~ EOI }
|
file = ${ SOI ~ (empty_line* ~ command)* ~ empty_line* ~ WHITESPACE* ~ EOI }
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
use std::result;
|
use std::result;
|
||||||
|
|
||||||
use chrono::NaiveDate;
|
use chrono::NaiveDate;
|
||||||
|
use chrono_tz::Tz;
|
||||||
use pest::error::ErrorVariant;
|
use pest::error::ErrorVariant;
|
||||||
use pest::iterators::Pair;
|
use pest::iterators::Pair;
|
||||||
use pest::prec_climber::{Assoc, Operator, PrecClimber};
|
use pest::prec_climber::{Assoc, Operator, PrecClimber};
|
||||||
|
|
@ -32,6 +33,22 @@ fn fail<S: Into<String>, T>(span: Span, message: S) -> Result<T> {
|
||||||
Err(error(span, message))
|
Err(error(span, message))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_include(p: Pair<Rule>) -> String {
|
||||||
|
assert_eq!(p.as_rule(), Rule::include);
|
||||||
|
p.into_inner().next().unwrap().as_str().to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_timezone(p: Pair<Rule>) -> Result<Tz> {
|
||||||
|
assert_eq!(p.as_rule(), Rule::timezone);
|
||||||
|
let span = p.as_span();
|
||||||
|
p.into_inner()
|
||||||
|
.next()
|
||||||
|
.unwrap()
|
||||||
|
.as_str()
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| error(span, "invalid timezone"))
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_number(p: Pair<Rule>) -> i32 {
|
fn parse_number(p: Pair<Rule>) -> i32 {
|
||||||
assert_eq!(p.as_rule(), Rule::number);
|
assert_eq!(p.as_rule(), Rule::number);
|
||||||
p.as_str().parse().unwrap()
|
p.as_str().parse().unwrap()
|
||||||
|
|
@ -691,34 +708,58 @@ fn parse_birthday(p: Pair<Rule>) -> Result<Birthday> {
|
||||||
Ok(Birthday { title, when, desc })
|
Ok(Birthday { title, when, desc })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_command(p: Pair<Rule>) -> Result<Command> {
|
fn parse_command(p: Pair<Rule>, file: &mut File) -> Result<()> {
|
||||||
assert_eq!(p.as_rule(), Rule::command);
|
assert_eq!(p.as_rule(), Rule::command);
|
||||||
|
|
||||||
let p = p.into_inner().next().unwrap();
|
let p = p.into_inner().next().unwrap();
|
||||||
match p.as_rule() {
|
match p.as_rule() {
|
||||||
Rule::task => parse_task(p).map(Command::Task),
|
Rule::include => {
|
||||||
Rule::note => parse_note(p).map(Command::Note),
|
// Since we've successfully opened the file, its name can't be the
|
||||||
Rule::birthday => parse_birthday(p).map(Command::Birthday),
|
// root directory or empty string and must thus have a parent.
|
||||||
|
let parent = file.name.parent().unwrap();
|
||||||
|
file.includes.push(parent.join(parse_include(p)));
|
||||||
|
}
|
||||||
|
Rule::timezone => match file.timezone {
|
||||||
|
None => file.timezone = Some(parse_timezone(p)?),
|
||||||
|
Some(_) => fail(p.as_span(), "cannot set timezone multiple times")?,
|
||||||
|
},
|
||||||
|
Rule::task => file.commands.push(Command::Task(parse_task(p)?)),
|
||||||
|
Rule::note => file.commands.push(Command::Note(parse_note(p)?)),
|
||||||
|
Rule::birthday => file.commands.push(Command::Birthday(parse_birthday(p)?)),
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse(path: PathBuf, input: &str) -> Result<File> {
|
pub fn parse_file(p: Pair<Rule>, name: PathBuf) -> Result<File> {
|
||||||
let pathstr = path.to_string_lossy();
|
assert_eq!(p.as_rule(), Rule::file);
|
||||||
let mut pairs = TodayfileParser::parse(Rule::file, input)?;
|
|
||||||
let file = pairs.next().unwrap();
|
|
||||||
let commands = file
|
|
||||||
.into_inner()
|
|
||||||
// For some reason, the EOI in `file` always gets captured
|
|
||||||
.take_while(|p| p.as_rule() == Rule::command)
|
|
||||||
.map(parse_command)
|
|
||||||
.collect::<Result<_>>()
|
|
||||||
.map_err(|e| e.with_path(&pathstr))?;
|
|
||||||
|
|
||||||
Ok(File {
|
let mut file = File {
|
||||||
name: path,
|
name,
|
||||||
includes: vec![],
|
includes: vec![],
|
||||||
timezone: None,
|
timezone: None,
|
||||||
commands,
|
commands: vec![],
|
||||||
})
|
};
|
||||||
|
|
||||||
|
for p in p.into_inner() {
|
||||||
|
// For some reason, the EOI in `file` always gets captured
|
||||||
|
if p.as_rule() == Rule::EOI {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_command(p, &mut file)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(path: &Path, input: &str) -> Result<File> {
|
||||||
|
let pathstr = path.to_string_lossy();
|
||||||
|
|
||||||
|
let mut pairs = TodayfileParser::parse(Rule::file, input).map_err(|e| e.with_path(&pathstr))?;
|
||||||
|
let file_pair = pairs.next().unwrap();
|
||||||
|
assert_eq!(pairs.next(), None);
|
||||||
|
|
||||||
|
parse_file(file_pair, path.to_owned()).map_err(|e| e.with_path(&pathstr))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue