//! Parse and match a PageSpec. //! //! A [PageSpec](http://ikiwiki.info/ikiwiki/pagespec/) is an //! expression that selects pages using an expression. See the ikiwiki //! documentation for more. use crate::site::Site; use log::trace; use std::path::{Path, PathBuf}; use lalrpop_util::lalrpop_mod; lalrpop_mod!(pub pagespec); /// A parsed PageSpec expression, ready to be matched on page paths. #[derive(Debug)] pub struct PageSpec { container: PathBuf, expr: Box, } impl PageSpec { /// Create a new PageSpec. pub fn new(containing_page: &Path, spec: &str) -> Result { trace!( "PageSpec::new: containing_page={} spec={:?}", containing_page.display(), spec ); let expr = pagespec::ExprParser::new() .parse(spec) .map_err(|e| PageSpecError::Parse(format!("{}", e)))?; Ok(Self { container: containing_page.into(), expr, }) } /// Match a PageSpec on a page path. pub fn matches(&self, site: &Site, page_path: &Path) -> bool { trace!( "PageSpec::matches: container={} page_path={}", self.container.display(), page_path.display() ); assert!(page_path.is_absolute()); if let Ok(path) = page_path.strip_prefix(&self.container) { let path = format!("{}", path.display()); self.expr.matches(site, &self.container, &path) } else { false } } } /// Errors from PageSpec parsing. #[derive(Debug, thiserror::Error)] pub enum PageSpecError { #[error("failed to parse PageSpec expression: {0}")] Parse(String), } /// This is public only because parser generation needs it to be. #[derive(Debug)] pub enum Expr { Glob(String), LinksHereFunc(String), PageFunc(String), TaggedFunc(String), Negate(Box), Op(Box, OpCode, Box), } impl Expr { fn matches(&self, site: &Site, container: &Path, path: &str) -> bool { trace!("Expr::matches: path={:?} self={:?}", path, self); match self { Self::Glob(glob) => glob_matches(glob, path), Self::LinksHereFunc(glob) => links_here(site, container, path, glob), Self::PageFunc(glob) => page_matches(site, container, glob, path), // FIXME: check its page Self::TaggedFunc(glob) => tagged(site, container, glob, path), Self::Negate(expr) => !expr.matches(site, container, path), Self::Op(left, op, right) => match op { OpCode::And => { left.matches(site, container, path) && right.matches(site, container, path) } OpCode::Or => { left.matches(site, container, path) || right.matches(site, container, path) } }, } } } /// This is public only because parser generation needs it to be. #[derive(Debug)] pub enum OpCode { And, Or, } fn glob_matches(glob: &str, path: &str) -> bool { trace!("glob_matches: glob={:?} path={:?}", glob, path); let glob: Vec = glob.chars().collect(); let path: Vec = path.chars().collect(); if glob_matches_helper(&glob, &path) { trace!("glob_matches: match!"); true } else { trace!("glob_matches: no match"); false } } fn glob_matches_helper(mut glob: &[char], mut path: &[char]) -> bool { while !glob.is_empty() && !path.is_empty() { match glob[0] { '?' => { glob = &glob[1..]; path = &path[1..]; } '*' => { let glob_remain = &glob[1..]; if glob_remain.is_empty() { return true; } while !path.is_empty() { if glob_matches_helper(&glob[1..], path) { return true; } path = &path[1..]; } return false; } _ => { if glob[0] == path[0] { glob = &glob[1..]; path = &path[1..]; } else { return false; } } } } while let Some('*') = glob.first() { glob = &glob[1..]; } glob.is_empty() && path.is_empty() } fn links_here(site: &Site, container: &Path, path: &str, _glob: &str) -> bool { trace!( "links_here: container={} path={:?}", container.display(), path ); let path = Path::new(path); if let Some(page) = site.page(path) { trace!("links_here: found page {}", path.display()); for link in page.meta().links_to() { trace!("links_here: page links to {}", link.display()); if link == container { trace!("links to container!"); return true; } } } trace!("links_here: does not link to container"); false } fn tagged(_site: &Site, container: &Path, path: &str, _glob: &str) -> bool { trace!("tagged: container={} path={:?}", container.display(), path); false } fn page_matches(site: &Site, container: &Path, glob: &str, path: &str) -> bool { if glob_matches(glob, path) { let full_path = container.join(path); site.is_page(&full_path) } else { false } } #[cfg(test)] mod test { use super::*; #[test] fn fixed_glob_matches_itself() { assert!(glob_matches("foo", "foo")); } #[test] fn fixed_glob_doesnt_match_other_string() { assert!(!glob_matches("foo", "bar")); } #[test] fn glob_question_mark_matches() { assert!(glob_matches("foo?", "foo0")); } #[test] fn glob_question_mark_doesnt_match_too_long_string() { assert!(!glob_matches("foo?", "foo")); } #[test] fn glob_star_matches_empty_string_at_beginning() { assert!(glob_matches("*foo", "foo")); } #[test] fn glob_star_matches_empty_string_in_middle() { assert!(glob_matches("foo*bar", "foobar")); } #[test] fn glob_star_matches_empty_string_at_end() { assert!(glob_matches("foo*", "foo")); } #[test] fn glob_star_matches_nonempty_string_at_beginning() { assert!(glob_matches("*bar", "foobar")); } #[test] fn glob_star_matches_nonempty_string_at_end() { assert!(glob_matches("foo*", "foobar")); } #[test] fn glob_star_matches_nonempty_string_in_middle() { assert!(glob_matches("foo*bar", "fooyobar")); } #[test] fn spec_fixed_page_name_matches_itself() { let site = Site::new("/src", "/dest"); let spec = PageSpec::new(Path::new("/"), "foo").unwrap(); assert!(spec.matches(&site, Path::new("/foo"))); } #[test] fn spec_glob_matches_page() { let site = Site::new("/src", "/dest"); let spec = PageSpec::new(Path::new("/"), "foo*").unwrap(); assert!(spec.matches(&site, Path::new("/foobar"))); } #[test] fn spec_negation() { let site = Site::new("/src", "/dest"); let spec = PageSpec::new(Path::new("/"), "!foo*").unwrap(); assert!(!spec.matches(&site, Path::new("/foo"))); assert!(spec.matches(&site, Path::new("/bar"))); } #[test] fn spec_or_matches_either() { let site = Site::new("/src", "/dest"); let spec = PageSpec::new(Path::new("/"), "foo or bar").unwrap(); assert!(spec.matches(&site, Path::new("/foo"))); assert!(spec.matches(&site, Path::new("/bar"))); assert!(!spec.matches(&site, Path::new("/yo"))); } #[test] fn spec_and_matches_both() { let site = Site::new("/src", "/dest"); let spec = PageSpec::new(Path::new("/"), "foo and !bar").unwrap(); assert!(spec.matches(&site, Path::new("/foo"))); assert!(!spec.matches(&site, Path::new("/bar"))); assert!(!spec.matches(&site, Path::new("/yo"))); } }