use crate::page::PageMeta; use crate::site::Site; use crate::wikitext::ParsedDirective; use log::{debug, trace}; use std::collections::HashSet; #[derive(Debug, thiserror::Error)] pub enum DirectiveError { #[error("unknown directive {0}")] UnknownDirective(String), #[error("directive {0} is missing required argument {1}")] DirectiveMissingArg(String, String), #[error("directive {0} has unknown argument {1}")] DirectiveUnknownArg(String, String), #[error("directive {0} does not allow unnamed arguments")] UnknownArgsNotAllowed(String), #[error("directive isn't implemented yet: {0}")] UnimplementedDirective(String), #[error("toc directive arguments 'levels' could not be parsed as an integer: {0}")] LevelsParse(String, #[source] std::num::ParseIntError), #[error(transparent)] PageSpec(#[from] crate::pagespec::PageSpecError), #[error(transparent)] Time(#[from] crate::time::TimeError), #[error(transparent)] Site(#[from] Box), } pub enum Processed { Markdown(String), Toc(usize), } pub trait DirectiveImplementation { const REQUIRED: &'static [&'static str]; const ALLOWED: &'static [&'static str]; const ALLOW_ANY_UNNAMED: bool; fn from_parsed(p: &ParsedDirective) -> Self; fn process(&self, site: &Site, meta: &mut PageMeta) -> Result; fn prepare(&self, _site: &mut Site) -> Result<(), DirectiveError> { Ok(()) } } #[derive(Debug, Eq, PartialEq)] pub enum Directive { Simple, UnnamedArg, SimpleArg, QuotedArg, MultilineArg, Brokenlinks(Brokenlinks), Calendar(Calendar), Format(Format), Graph(Graph), Img(Img), Inline(Inline), Map(Map), Meta(Meta), PageStats(PageStats), Shortcut(Shortcut), Sidebar(Sidebar), Table(Table), Tag(Tag), Toc(Toc), TrailLink(TrailLink), } impl TryFrom for Directive { type Error = DirectiveError; fn try_from(p: ParsedDirective) -> Result { Self::try_from(&p) } } impl TryFrom<&ParsedDirective> for Directive { type Error = DirectiveError; fn try_from(p: &ParsedDirective) -> Result { let d = match p.name() { "simple" => { Self::check_args(p, &[], &[], false)?; Directive::Simple } "unnamedarg" => { Self::check_args(p, &[], &[], true)?; Directive::UnnamedArg } "simplearg" => { Self::check_args(p, &["foo"], &[], false)?; Directive::SimpleArg } "quotedarg" => { Self::check_args(p, &["bar"], &[], false)?; Directive::QuotedArg } "multilinearg" => { Self::check_args(p, &["yo"], &["then", "else"], false)?; Directive::MultilineArg } "brokenlinks" => { Self::check_args( p, Brokenlinks::REQUIRED, Brokenlinks::ALLOWED, Brokenlinks::ALLOW_ANY_UNNAMED, )?; Directive::Brokenlinks(Brokenlinks::from_parsed(p)) } "calendar" => { Self::check_args( p, Calendar::REQUIRED, Calendar::ALLOWED, Calendar::ALLOW_ANY_UNNAMED, )?; Directive::Calendar(Calendar::from_parsed(p)) } "format" => { Self::check_args( p, Format::REQUIRED, Format::ALLOWED, Format::ALLOW_ANY_UNNAMED, )?; Directive::Format(Format::from_parsed(p)) } "graph" => { Self::check_args(p, Graph::REQUIRED, Graph::ALLOWED, Graph::ALLOW_ANY_UNNAMED)?; Directive::Graph(Graph::from_parsed(p)) } "img" => { Self::check_args(p, Img::REQUIRED, Img::ALLOWED, Img::ALLOW_ANY_UNNAMED)?; Directive::Img(Img::from_parsed(p)) } "inline" => { Self::check_args( p, Inline::REQUIRED, Inline::ALLOWED, Inline::ALLOW_ANY_UNNAMED, )?; Directive::Inline(Inline::from_parsed(p)) } "map" => { Self::check_args(p, Map::REQUIRED, Map::ALLOWED, Map::ALLOW_ANY_UNNAMED)?; Directive::Map(Map::from_parsed(p)) } "meta" => { Self::check_args(p, Meta::REQUIRED, Meta::ALLOWED, Meta::ALLOW_ANY_UNNAMED)?; Directive::Meta(Meta::from_parsed(p)) } "pagestats" => { Self::check_args( p, PageStats::REQUIRED, PageStats::ALLOWED, PageStats::ALLOW_ANY_UNNAMED, )?; Directive::PageStats(PageStats::from_parsed(p)) } "shortcut" => { Self::check_args( p, Shortcut::REQUIRED, Shortcut::ALLOWED, Shortcut::ALLOW_ANY_UNNAMED, )?; Directive::Shortcut(Shortcut::from_parsed(p)) } "sidebar" => { Self::check_args( p, Sidebar::REQUIRED, Sidebar::ALLOWED, Sidebar::ALLOW_ANY_UNNAMED, )?; Directive::Sidebar(Sidebar::from_parsed(p)) } "tag" => { Self::check_args(p, Tag::REQUIRED, Tag::ALLOWED, Tag::ALLOW_ANY_UNNAMED)?; Directive::Tag(Tag::from_parsed(p)) } "table" => { Self::check_args(p, Table::REQUIRED, Table::ALLOWED, Table::ALLOW_ANY_UNNAMED)?; Directive::Table(Table::from_parsed(p)) } "toc" => { Self::check_args(p, Toc::REQUIRED, Toc::ALLOWED, Toc::ALLOW_ANY_UNNAMED)?; Directive::Toc(Toc::from_parsed(p)) } "traillink" => { Self::check_args( p, TrailLink::REQUIRED, TrailLink::ALLOWED, TrailLink::ALLOW_ANY_UNNAMED, )?; Directive::TrailLink(TrailLink::from_parsed(p)) } _ => return Err(DirectiveError::UnknownDirective(p.name().into())), }; Ok(d) } } impl Directive { fn check_args( p: &ParsedDirective, required: &[&str], allowed: &[&str], allow_any_unnamed: bool, ) -> Result<(), DirectiveError> { let args = p.args(); for arg in required.iter() { if !args.contains_key(arg) { return Err(DirectiveError::DirectiveMissingArg( p.name().into(), arg.to_string(), )); } } let required: HashSet = required.iter().map(|arg| arg.to_string()).collect(); let allowed: HashSet = allowed.iter().map(|arg| arg.to_string()).collect(); let allowed: HashSet = required.union(&allowed).cloned().collect(); for (arg, value) in p.args().iter() { if value.is_empty() { if !allow_any_unnamed { return Err(DirectiveError::UnknownArgsNotAllowed(p.name().into())); } } else if !allowed.contains(*arg) { debug!("parsed directive {:?}", p); return Err(DirectiveError::DirectiveUnknownArg( p.name().into(), arg.to_string(), )); } } Ok(()) } pub fn prepare(&self, site: &mut Site) -> Result<(), DirectiveError> { trace!("prepare directive {:?}", self); if let Self::Shortcut(x) = self { x.prepare(site)?; } Ok(()) } pub fn process( &self, site: &mut Site, meta: &mut PageMeta, ) -> Result { match self { Self::Simple | Self::UnnamedArg | Self::SimpleArg | Self::QuotedArg | Self::MultilineArg => { panic!("directive {:?} may only be used in parsing tests", self) } Self::Brokenlinks(x) => x.process(site, meta), Self::Calendar(x) => x.process(site, meta), Self::Format(x) => x.process(site, meta), Self::Graph(x) => x.process(site, meta), Self::Img(x) => x.process(site, meta), Self::Inline(x) => x.process(site, meta), Self::Map(x) => x.process(site, meta), Self::Meta(x) => x.process(site, meta), Self::PageStats(x) => x.process(site, meta), Self::Shortcut(x) => x.process(site, meta), Self::Sidebar(x) => x.process(site, meta), Self::Table(x) => x.process(site, meta), Self::Tag(x) => x.process(site, meta), Self::Toc(x) => x.process(site, meta), Self::TrailLink(x) => x.process(site, meta), } } } mod format; use format::Format; mod meta; use meta::Meta; mod map; use map::Map; mod graph; use graph::Graph; mod img; pub use img::Img; mod inline; pub use inline::Inline; mod pagestats; pub use pagestats::PageStats; mod shortcut; pub use shortcut::Shortcut; mod table; use table::Table; mod tag; use tag::Tag; mod toc; pub use toc::Toc; mod traillink; use traillink::TrailLink; mod calendar; use calendar::Calendar; mod sidebar; use sidebar::Sidebar; mod brokenlinks; use brokenlinks::Brokenlinks;