From 34954ddef238290cff4102667462162fa82c6a6c Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 4 Apr 2024 17:00:28 -0700 Subject: [PATCH] Harper + Mockingbird 0.0.2. This release includes the following major changes: * `derive_more` was removed as a dependency. * Collections are now associated with a name. * Internal refactor for a new `OwnedEntry`. * Updated to minijinja2. * Undefined variables now result in errors. * Added a `get()` template helper with a default when undefined. * Added a real CLI parser. * Added a `time!()` macro for wall-clock time measurements. * Now exits with status 1 if rendering fails. --- CHANGELOG.md | 4 + lib/Cargo.toml | 8 +- lib/src/fstree.rs | 58 ++++--- lib/src/markdown/alias.rs | 4 +- lib/src/markdown/auto_heading.rs | 12 +- lib/src/markdown/code_filter.rs | 6 +- lib/src/markdown/highlight.rs | 4 +- lib/src/markdown/parts.rs | 4 +- lib/src/markdown/plugin.rs | 7 +- lib/src/markdown/render.rs | 7 +- lib/src/markdown/snippet.rs | 6 +- lib/src/markdown/toc.rs | 7 +- lib/src/markdown/ts_highlight.rs | 1 - lib/src/taxonomy/collection.rs | 27 ++- lib/src/taxonomy/item.rs | 18 +- lib/src/taxonomy/metadata.rs | 15 +- lib/src/taxonomy/renderer.rs | 4 +- lib/src/taxonomy/site.rs | 30 ++-- lib/src/templating/minijinja.rs | 281 ++++++++++++++----------------- lib/src/util/variation.rs | 1 + lib/src/value/list.rs | 11 +- lib/src/value/mapper.rs | 2 +- mockingbird/Cargo.toml | 5 +- mockingbird/src/config.rs | 13 +- mockingbird/src/discover.rs | 18 +- mockingbird/src/main.rs | 74 +++++--- mockingbird/src/render.rs | 16 +- mockingbird/src/util.rs | 11 ++ 28 files changed, 343 insertions(+), 311 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64ac396..55f0744 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# Version 0.0.2 (Apr 4, 2024) + +This is the second testing release of Mockingbird. More details to come. + # Version 0.0.1 (Mar 3, 2024) This is the initial testing release of Mockingbird. More details to come. diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 90c6b30..7ba4cf2 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "harper" -version = "0.0.1" +version = "0.0.2" edition = "2021" authors = ["Sergio Benitez "] license = "MIT OR Apache-2.0" @@ -23,7 +23,6 @@ serde = { version = "1", features = ["rc", "derive"] } toml = { version = "0.8", features = ["preserve_order"] } memchr = "2" either = "1.10" -derive_more = { version = "=1.0.0-beta.6", features = ["debug", "deref", "from"] } grass = { version = "0.13", default-features = false, features = ["random"], optional = true } pulldown-cmark = { version = "0.10", default-features = false, features = ["simd", "html"] } @@ -56,11 +55,8 @@ default-features = false features = ["html", "default-syntaxes", "regex-onig", "plist-load"] [dependencies.minijinja] -# git = "https://github.com/SergioBenitez/minijinja.git" -# # branch = "value-unification" -# rev = "517d147" package = "unified-minijinja" -version = "1.0.12" +version = "=0.0.2" default-features = false features = ["speedups", "loader", "builtins", "debug", "deserialization", "macros", "multi_template"] diff --git a/lib/src/fstree.rs b/lib/src/fstree.rs index 0195c42..b690815 100644 --- a/lib/src/fstree.rs +++ b/lib/src/fstree.rs @@ -1,24 +1,26 @@ +use std::{fs, fmt}; +use std::ops::Deref; use std::sync::Arc; use std::path::Path; use std::collections::VecDeque; -use std::{fs, fmt}; use rustc_hash::FxHashMap; use crate::error::Result; -#[derive(Copy, Clone, PartialEq, Eq, Hash)] -pub struct EntryId(pub(crate) usize); - #[derive(Debug)] pub struct FsTree { entries: Vec, map: FxHashMap, EntryId>, } -pub struct FsSubTree<'a> { - tree: &'a FsTree, - root: EntryId +#[derive(Copy, Clone, PartialEq, Eq, Hash)] +pub struct EntryId(usize); + +#[derive(Clone)] +pub struct OwnedEntry { + pub tree: Arc, + pub id: EntryId, } #[derive(Debug)] @@ -44,10 +46,12 @@ impl FsTree { } } + #[inline(always)] pub fn build>(root: P) -> Result { Self::build_with(root.as_ref(), |_, _| Ok(())) } + #[inline] pub fn build_with(root: P, mut callback: F) -> Result where P: AsRef, F: FnMut(&Self, EntryId) -> Result<()>, @@ -91,12 +95,6 @@ impl FsTree { EntryId(0) } - #[inline] - pub fn subtree(&self, root: EntryId) -> FsSubTree<'_> { - assert!(self[root].id == root); - FsSubTree { tree: self, root } - } - #[inline] pub fn get(&self, root: R, path: P) -> Option<&Entry> where R: Into>, P: AsRef @@ -227,22 +225,38 @@ impl FsTree { } } -impl FsSubTree<'_> { - pub fn get>(&self, path: P) -> Option<&Entry> { - self.tree.get(self.root, path.as_ref()) +impl OwnedEntry { + pub fn new(tree: Arc, id: EntryId) -> Self { + OwnedEntry { tree, id } } - #[inline] - pub fn get_file_id>(&self, path: P) -> Option { - self.tree.get_file_id(self.root, path.as_ref()) + pub fn entry(&self) -> &Entry { + &self.tree[self.id] } +} - #[inline] - pub fn get_id>(&self, path: P) -> Option { - self.tree.get_id(self.root, path.as_ref()) +impl Deref for OwnedEntry { + type Target = Entry; + + fn deref(&self) -> &Self::Target { + self.entry() + } +} + +impl fmt::Debug for OwnedEntry { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.entry().fmt(f) + } +} + +impl PartialEq for OwnedEntry { + fn eq(&self, other: &Self) -> bool { + self.id == other.id } } +impl Eq for OwnedEntry { } + impl Entry { /// File name without the extension. pub fn file_stem(&self) -> &str { diff --git a/lib/src/markdown/alias.rs b/lib/src/markdown/alias.rs index 5a298be..de56862 100644 --- a/lib/src/markdown/alias.rs +++ b/lib/src/markdown/alias.rs @@ -19,10 +19,10 @@ impl<'a> Alias<'a> { } impl crate::markdown::Plugin for Alias<'_> { - fn remap<'a, I>(&'a mut self, events: I) -> Box> + 'a> + fn remap<'a, I>(&'a mut self, events: I) -> impl Iterator> + 'a where I: Iterator> + 'a { - Box::new(AliasIterator { inner: events, map: self.map }) + AliasIterator { inner: events, map: self.map } } } diff --git a/lib/src/markdown/auto_heading.rs b/lib/src/markdown/auto_heading.rs index eabb249..27440dc 100644 --- a/lib/src/markdown/auto_heading.rs +++ b/lib/src/markdown/auto_heading.rs @@ -54,14 +54,14 @@ impl<'a, I: Iterator>> Iterator for HeadingIterator<'a, I> { } impl Plugin for AutoHeading { - fn remap<'a, I>(&'a mut self, events: I) -> Box> + 'a> + fn remap<'a, I>(&'a mut self, events: I) -> impl Iterator> + 'a where I: Iterator> + 'a { - Box::new(HeadingIterator { + HeadingIterator { seen: FxHashMap::default(), inner: events, stack: VecDeque::with_capacity(4), - }) + } } } @@ -92,12 +92,12 @@ impl<'a, I: Iterator>> Iterator for AnchorIterator<'a, I> { } impl Plugin for HeadingAnchor { - fn remap<'a, I>(&'a mut self, events: I) -> Box> + 'a> + fn remap<'a, I>(&'a mut self, events: I) -> impl Iterator> + 'a where I: Iterator> + 'a { - Box::new(AnchorIterator { + AnchorIterator { inner: events, pending: None, - }) + } } } diff --git a/lib/src/markdown/code_filter.rs b/lib/src/markdown/code_filter.rs index f107ccf..4c146b4 100644 --- a/lib/src/markdown/code_filter.rs +++ b/lib/src/markdown/code_filter.rs @@ -40,15 +40,15 @@ impl CodeTrim<()> { } impl Plugin for CodeTrim { - fn remap<'a, I>(&'a mut self, i: I) -> Box> + 'a> + fn remap<'a, I>(&'a mut self, i: I) -> impl Iterator> + 'a where I: Iterator> + 'a { - Box::new(Iter { + Iter { trimmer: &mut self.trimmer, inner: i, line_num: None, stack: VecDeque::new(), - }) + } } } diff --git a/lib/src/markdown/highlight.rs b/lib/src/markdown/highlight.rs index 8266db5..a915854 100644 --- a/lib/src/markdown/highlight.rs +++ b/lib/src/markdown/highlight.rs @@ -31,10 +31,10 @@ impl SyntaxHighlight { } impl Plugin for SyntaxHighlight { - fn remap<'a, I>(&'a mut self, events: I) -> Box> + 'a> + fn remap<'a, I>(&'a mut self, events: I) -> impl Iterator> + 'a where I: Iterator> + 'a { - Box::new(Highlighter { generator: None, lines: 0, inner: events }) + Highlighter { generator: None, lines: 0, inner: events } } } diff --git a/lib/src/markdown/parts.rs b/lib/src/markdown/parts.rs index 21e8055..e2273b0 100644 --- a/lib/src/markdown/parts.rs +++ b/lib/src/markdown/parts.rs @@ -55,7 +55,7 @@ impl Plugin for Parts { // secion. This causes the HTML renderer to emit the string so far. We then // reuse the same iterator in the renderer again until it signals it cannot // be reused. We finally return one string containing all of the HTML. - fn remap<'a, I>(&'a mut self, events: I) -> Box> + 'a> + fn remap<'a, I>(&'a mut self, events: I) -> impl Iterator> + 'a where I: Iterator> + 'a { let mut sections = SectionIterator { @@ -84,7 +84,7 @@ impl Plugin for Parts { _ => self.sections.join("").into(), }; - Box::new(Some(Event::Html(complete_html)).into_iter()) + Some(Event::Html(complete_html)).into_iter() } fn finalize(&mut self) -> Result<()> { diff --git a/lib/src/markdown/plugin.rs b/lib/src/markdown/plugin.rs index eda4efc..7033388 100644 --- a/lib/src/markdown/plugin.rs +++ b/lib/src/markdown/plugin.rs @@ -10,14 +10,11 @@ pub trait Plugin { Ok(Cow::Borrowed(input)) } - // FIXME: To get rid of this box, we need (ideally) `-> impl Trait` in trait - // methods, or generic associated types. Edit: We now have GATs! But they're - // incredibly annoying to use because of the required bounds everywhere. #[inline(always)] - fn remap<'a, I>(&'a mut self, events: I) -> Box> + 'a> + fn remap<'a, I>(&'a mut self, events: I) -> impl Iterator> + 'a where I: Iterator> + 'a { - Box::new(events) + events } #[inline(always)] diff --git a/lib/src/markdown/render.rs b/lib/src/markdown/render.rs index 8eda19c..dc62878 100644 --- a/lib/src/markdown/render.rs +++ b/lib/src/markdown/render.rs @@ -17,17 +17,16 @@ impl Renderer { } impl Plugin for Renderer { - fn remap<'a, I>(&'a mut self, events: I) -> Box> + 'a> + fn remap<'a, I>(&'a mut self, events: I) -> impl Iterator> + 'a where I: Iterator> { let mut html_output = String::new(); html::push_html(&mut html_output, events); self.rendered = html_output; - Box::new(std::iter::empty()) + std::iter::empty() } fn finalize(&mut self) -> Result<()> { - let string = std::mem::replace(&mut self.rendered, String::new()); - self.output.write(string) + self.output.write(std::mem::take(&mut self.rendered)) } } diff --git a/lib/src/markdown/snippet.rs b/lib/src/markdown/snippet.rs index ef40fad..25dda53 100644 --- a/lib/src/markdown/snippet.rs +++ b/lib/src/markdown/snippet.rs @@ -103,17 +103,17 @@ impl<'a, I: Iterator>> Iterator for SnippetIterator<'a, I> { // IDEA: What if we just take the first k character of the text and render that as markdown? impl Plugin for Snippet { - fn remap<'a, I>(&'a mut self, events: I) -> Box> + 'a> + fn remap<'a, I>(&'a mut self, events: I) -> impl Iterator> + 'a where I: Iterator> + 'a { - Box::new(SnippetIterator { + SnippetIterator { snippet: &mut self.snippet, snip_text_len: 0, inner: events, capture: vec![], min_length: self.length, done: self.length == 0, - }) + } } fn finalize(&mut self) -> Result<()> { diff --git a/lib/src/markdown/toc.rs b/lib/src/markdown/toc.rs index 5728bc6..f3deecc 100644 --- a/lib/src/markdown/toc.rs +++ b/lib/src/markdown/toc.rs @@ -52,11 +52,12 @@ impl TableOfContents { } impl Plugin for TableOfContents { - fn remap<'a, I>(&'a mut self, events: I) -> Box> + 'a> + fn remap<'a, I>(&'a mut self, events: I) -> impl Iterator> + 'a where I: Iterator> + 'a { self.reset(); - Box::new(events.inspect(|ev| match ev { + + events.inspect(|ev| match ev { Event::Start(Tag::Heading { level, id, .. }) => { self.entry = Some(Entry { title: String::new(), @@ -75,7 +76,7 @@ impl Plugin for TableOfContents { } } _ => {} - })) + }) } fn finalize(&mut self) -> Result<()> { diff --git a/lib/src/markdown/ts_highlight.rs b/lib/src/markdown/ts_highlight.rs index caba85f..015a6eb 100644 --- a/lib/src/markdown/ts_highlight.rs +++ b/lib/src/markdown/ts_highlight.rs @@ -133,7 +133,6 @@ impl<'a, I: Iterator>> Iterator for Highlighter { .unwrap_or(&*label); self.code = String::new(); - // self.config = time!(find_ts_highlight_config(lang)); self.config = find_ts_highlight_config(lang); } Event::Text(text) if self.config.is_some() => { diff --git a/lib/src/taxonomy/collection.rs b/lib/src/taxonomy/collection.rs index 7764f41..fe189d0 100644 --- a/lib/src/taxonomy/collection.rs +++ b/lib/src/taxonomy/collection.rs @@ -2,18 +2,15 @@ use std::sync::Arc; use rayon::prelude::*; use rustc_hash::FxHashMap; -use derive_more::Debug; -use crate::fstree::{Entry, EntryId, FsTree}; +use crate::fstree::{Entry, EntryId, FsTree, OwnedEntry}; use crate::value::List; use crate::taxonomy::*; #[derive(Debug)] pub struct Collection { - #[debug(ignore)] - pub tree: Arc, - #[debug("{:?}", tree[**root])] - pub root: EntryId, + pub entry: OwnedEntry, + pub name: Arc, pub index: Option>, pub items: Arc>>, pub data: FxHashMap>>>, @@ -32,34 +29,34 @@ pub enum Kind { } impl Collection { - pub fn new(tree: Arc, root: EntryId) -> Collection { + pub fn new(name: Arc, tree: Arc, root: EntryId) -> Collection { Collection { - tree, - root, + name, + entry: OwnedEntry::new(tree, root), index: None, items: Default::default(), data: Default::default(), } } - pub fn root_entry(&self) -> &Entry { - &self.tree[self.root] + pub fn entry(&self) -> &Entry { + &self.entry } - pub fn new_item(&mut self, entry: EntryId) -> Arc { - let item = Arc::new(Item::new(self.tree.clone(), entry)); + pub fn new_item(&mut self, id: EntryId) -> Arc { + let item = Arc::new(Item::new(self.entry.tree.clone(), id)); self.items.push(item.clone()); item } pub fn new_datum(&mut self, parent: EntryId, entry: EntryId) -> Arc { - let datum = Arc::new(Item::new(self.tree.clone(), entry)); + let datum = Arc::new(Item::new(self.entry.tree.clone(), entry)); self.data.entry(parent).or_default().push(datum.clone()); datum } pub fn set_index_item(&mut self, entry: EntryId) -> Arc { - let index = Arc::new(Item::new(self.tree.clone(), entry)); + let index = Arc::new(Item::new(self.entry.tree.clone(), entry)); self.index = Some(index.clone()); index } diff --git a/lib/src/taxonomy/item.rs b/lib/src/taxonomy/item.rs index a2fa667..f024a74 100644 --- a/lib/src/taxonomy/item.rs +++ b/lib/src/taxonomy/item.rs @@ -1,30 +1,20 @@ use std::sync::Arc; -use derive_more::Debug; - -use crate::fstree::{Entry, EntryId, FsTree}; +use crate::fstree::{EntryId, FsTree, OwnedEntry}; use crate::taxonomy::*; #[derive(Debug, Clone)] pub struct Item { - #[debug(skip)] - pub tree: Arc, - #[debug("{:?}", &tree[**id])] - pub id: EntryId, + pub entry: OwnedEntry, // TODO: Do we need private metadata that the user can't touch? pub metadata: Metadata, } impl Item { - pub(crate) fn new(tree: Arc, entry: EntryId) -> Self { + pub(crate) fn new(tree: Arc, id: EntryId) -> Self { Self { - tree, - id: entry, + entry: OwnedEntry::new(tree, id), metadata: Metadata::new(), } } - - pub fn entry(&self) -> &Entry { - &self.tree[self.id] - } } diff --git a/lib/src/taxonomy/metadata.rs b/lib/src/taxonomy/metadata.rs index dd5332c..6995001 100644 --- a/lib/src/taxonomy/metadata.rs +++ b/lib/src/taxonomy/metadata.rs @@ -3,8 +3,6 @@ use std::borrow::Borrow; use std::marker::PhantomData; use std::sync::Arc; -use derive_more::Debug; - use crate::value::{Source, Sink}; use crate::error::Result; use crate::value::Value; @@ -14,7 +12,7 @@ type Hasher = std::hash::BuildHasherDefault; pub trait MetaKey: 'static { const KEY: &'static str; - type Value: TryFrom + Into + Debug; + type Value: TryFrom + Into + fmt::Debug; } #[macro_export] @@ -31,7 +29,7 @@ macro_rules! define_meta_key { } } -#[derive(Debug, Clone)] +#[derive(Clone)] pub struct Key<'m, 'k, V> { map: &'m Metadata, key: &'k str, @@ -171,6 +169,15 @@ impl Metadata { // } } +impl fmt::Debug for Key<'_, '_, V> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Key") + .field("map", &self.map) + .field("key", &self.key) + .finish() + } +} + impl fmt::Display for Metadata { #[inline(always)] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { diff --git a/lib/src/taxonomy/renderer.rs b/lib/src/taxonomy/renderer.rs index 22fc481..82645c9 100644 --- a/lib/src/taxonomy/renderer.rs +++ b/lib/src/taxonomy/renderer.rs @@ -32,9 +32,9 @@ pub fn render_collection( where R: Renderer + ?Sized { rayon::join( - || collection.items.sort_by(|a, b| a.entry().path.cmp(&b.entry().path)), + || collection.items.sort_by(|a, b| a.entry.path.cmp(&b.entry.path)), || collection.data.par_iter().for_each(|(_, l)| { - l.sort_by(|a, b| a.entry().path.cmp(&b.entry().path)) + l.sort_by(|a, b| a.entry.path.cmp(&b.entry.path)) }), ); diff --git a/lib/src/taxonomy/site.rs b/lib/src/taxonomy/site.rs index 7b28684..09183d4 100644 --- a/lib/src/taxonomy/site.rs +++ b/lib/src/taxonomy/site.rs @@ -1,6 +1,5 @@ use std::sync::Arc; -use derive_more::Debug; use rustc_hash::FxHashMap; use crate::fstree::{EntryId, FsTree}; @@ -8,20 +7,29 @@ use crate::taxonomy::*; #[derive(Debug)] pub struct Site { - #[debug(ignore)] pub tree: Arc, pub items: Vec>, pub collections: FxHashMap>, + pub index: FxHashMap, EntryId>, } impl Site { pub fn new(tree: Arc) -> Site { - Site { tree, collections: FxHashMap::default(), items: vec![] } + Site { tree, items: vec![], collections: Default::default(), index: Default::default() } } - pub fn get_or_insert_collection(&mut self, root: EntryId) -> &mut Collection { - let arc = self.collections.entry(root) - .or_insert_with(|| Arc::new(Collection::new(self.tree.clone(), root))); + /// Panics if `name` is not unique to `root`. + pub fn get_or_insert_collection( + &mut self, + name: impl FnOnce() -> Arc, + root: EntryId + ) -> &mut Collection { + let arc = self.collections.entry(root).or_insert_with(|| { + let name = name(); + let collection = Arc::new(Collection::new(name.clone(), self.tree.clone(), root)); + assert!(self.index.insert(name, root).is_none()); + collection + }); Arc::get_mut(arc).expect("&mut -> &mut") } @@ -54,29 +62,29 @@ impl Site { for (i, collection) in self.collections.values().enumerate() { let i_sib = i < self.collections.len() - 1; - self.vis_heading(&[i_sib], collection.root, self.tree.root_id(), ""); + self.vis_heading(&[i_sib], collection.entry.id, self.tree.root_id(), ""); for (j, (&data_id, data_items)) in collection.data.iter().enumerate() { let j_sib = !collection.items.is_empty() || collection.index.is_some() || j < collection.data.len() - 1; - self.vis_heading(&[i_sib, j_sib], data_id, collection.root, "📦 "); + self.vis_heading(&[i_sib, j_sib], data_id, collection.entry.id, "📦 "); for (k, item) in data_items.iter().enumerate() { let k_sib = k < data_items.len() - 1; - self.vis_heading(&[i_sib, j_sib, k_sib], item.id, data_id, "💾 "); + self.vis_heading(&[i_sib, j_sib, k_sib], item.entry.id, data_id, "💾 "); } } if let Some(item) = &collection.index { let j_sib = !collection.items.is_empty(); - self.vis_heading(&[i_sib, j_sib], item.id, collection.root, "📑 "); + self.vis_heading(&[i_sib, j_sib], item.entry.id, collection.entry.id, "📑 "); } for (j, item) in collection.items.iter().enumerate() { let j_sib = j < collection.items.len() - 1; - self.vis_heading(&[i_sib, j_sib], item.id, collection.root, "📝 "); + self.vis_heading(&[i_sib, j_sib], item.entry.id, collection.entry.id, "📝 "); } } } diff --git a/lib/src/templating/minijinja.rs b/lib/src/templating/minijinja.rs index 4f6b2b3..c74848c 100644 --- a/lib/src/templating/minijinja.rs +++ b/lib/src/templating/minijinja.rs @@ -13,6 +13,7 @@ pub struct MiniJinjaEngine { env: Result>, } +#[derive(Debug)] pub struct SiteItem { pub site: Arc, pub collection: Option>, @@ -23,12 +24,12 @@ impl SiteItem { pub fn is_index(&self) -> bool { self.collection.as_ref() .and_then(|c| c.index.as_ref()) - .map_or(false, |i| i.id == self.item.id) + .map_or(false, |i| i.entry.id == self.item.entry.id) } pub fn position(&self) -> Option { self.collection.as_ref() - .and_then(|c| c.items.iter().position(|i| i.id == self.item.id)) + .and_then(|c| c.items.iter().position(|i| i.entry.id == self.item.entry.id)) } } @@ -38,6 +39,8 @@ fn try_init( globals: G, ) -> Result> { let mut env = Environment::new(); + env.set_undefined_behavior(minijinja::UndefinedBehavior::Strict); + if let Some(root) = root { env.set_loader(path_loader(&tree[root].path)); } @@ -86,6 +89,7 @@ fn try_init( env.add_filter("deslug", ext::deslug); env.add_filter("date", ext::date); env.add_filter("split", ext::split); + env.add_filter("get", ext::get); Ok(env) } @@ -113,7 +117,7 @@ impl Engine for MiniJinjaEngine { item: item.clone() }; - Ok(template.render(Value::from_map_object(site_item))?) + Ok(template.render(Value::from_object(site_item))?) } fn render_raw( @@ -131,7 +135,7 @@ impl Engine for MiniJinjaEngine { item: item.clone() }; - let context = Value::from_map_object(site_item); + let context = Value::from_object(site_item); let string = match name { Some(name) => env.render_named_str(name, template_str, context)?, None => env.render_str(template_str, context)?, @@ -147,7 +151,7 @@ impl Engine for MiniJinjaEngine { meta: Metadata, ) -> Result { let env = self.env.as_ref().map_err(|e| e.clone())?; - let context = Value::from_map_object(meta); + let context = Value::from_object(meta); let string = match name { Some(name) => env.render_named_str(name, template_str, context)?, None => env.render_str(template_str, context)?, @@ -161,7 +165,7 @@ mod ext { use std::sync::Arc; use chrono::{NaiveDate, NaiveTime, DateTime, Utc}; - use minijinja::{value::{intern, Rest, Value}, Error, ErrorKind, State}; + use minijinja::{value::{intern, DynObject, Rest, Value}, Error, ErrorKind, State}; use crate::url::Url; @@ -228,7 +232,7 @@ mod ext { use chrono::naive::NaiveDateTime; if let Ok(ts) = value.clone().try_into() { - let datetime = NaiveDateTime::from_timestamp_opt(ts, 0) + let datetime = DateTime::from_timestamp(ts, 0) .ok_or_else(|| Error::new( ErrorKind::InvalidOperation, "invalid timestamp provided to `date`" @@ -271,57 +275,48 @@ mod ext { .unwrap() .as_secs() } + + pub fn get(map: DynObject, key: &str, default: Value) -> Value { + map.get_value(&Value::from(key)).unwrap_or(default) + } } mod value_object { + use std::fmt::Debug; use std::sync::Arc; - use minijinja::value::{AnyMapObject, MapObject, SeqObject, Value}; + use minijinja::value::{DynObject, Enumerator, Object, ObjectExt, ObjectRepr, Value}; use crate::value; use crate::util::declare_variation; declare_variation!(Dict of value::Dict); - declare_variation!(Array of Vec); - impl MapObject for Dict { - fn get_field(self: &Arc, key: &Value) -> Option { - self.0.get(key.as_str()?) - .cloned() - .map(Value::from) + impl Object for Dict { + fn repr(self: &Arc) -> ObjectRepr { + ObjectRepr::Map } - fn fields(self: &Arc) -> Vec { - self.0.keys() - .cloned() - .map(Value::from) - .collect() - } - - fn field_count(self: &Arc) -> usize { - self.0.len() - } - } - - impl SeqObject for Array { - fn get_item(self: &Arc, idx: usize) -> Option { - self.0.get(idx) + fn get_value(self: &Arc, key: &Value) -> Option { + self.0.get(key.as_str()?) .cloned() .map(Value::from) } - fn item_count(self: &Arc) -> usize { - self.0.len() + fn enumerate(self: &Arc) -> Enumerator { + self.mapped_rev_enumerator(|this| Box::new({ + this.keys().cloned().map(Value::from) + })) } } - impl> SeqObject for value::List { - fn get_item(self: &Arc, idx: usize) -> Option { - let item = self.get(idx)?.clone(); - Some(Value::from(item.into())) + impl + Sync + Send> Object for value::List { + fn get_value(self: &Arc, key: &Value) -> Option { + let item = self.get(key.as_usize()?)?.clone(); + Some(Value::from_dyn_object(item.into())) } - fn item_count(self: &Arc) -> usize { - self.len() + fn enumerate(self: &Arc) -> Enumerator { + Enumerator::Seq(Self::len(self)) } } @@ -338,29 +333,37 @@ mod value_object { }, Value::String(s) => Self::from(s), Value::Path(s) => Self::from(s.into::>()), - Value::Array(a) => Self::from_any_seq_object(Array::new(a)), - Value::Dict(d) => Self::from_any_map_object(Dict::new(d)), + Value::Array(a) => Self::from_dyn_object(a), + Value::Dict(d) => Self::from_dyn_object(Dict::new(d)), } } } } mod taxonomy_object { - use std::{path::Path, sync::Arc}; - use minijinja::value::{intern, MapObject, SeqObject, Value}; + use std::{sync::Arc}; + use minijinja::value::{Enumerator, Object, ObjectExt, ObjectRepr, Value}; use super::SiteItem; - use crate::{declare_variation, taxonomy::{Collection, Item, Metadata, Site}}; + use crate::{declare_variation, taxonomy::{Collection, Item, Metadata, Site}, value::List}; + declare_variation!(SiteItems of Site); declare_variation!(SiteCollections of Site); + declare_variation!(CollectionItems of Collection); declare_variation!(CollectionData of Collection); - impl MapObject for SiteItem { + // FIXME: Use `this` or `item` to refer to the item to avoid key collisions + // between our keys here and the keys in `self.item`. + impl Object for SiteItem { + fn repr(self: &Arc) -> ObjectRepr { + ObjectRepr::Map + } + #[inline] - fn get_field(self: &Arc, name: &Value) -> Option { + fn get_value(self: &Arc, name: &Value) -> Option { let value = match name.as_str()? { - "site" => Value::from_any_map_object(self.site.clone()), - "collection" => Value::from_any_map_object(self.collection.as_ref()?.clone()), + "site" => Value::from_dyn_object(self.site.clone()), + "collection" => Value::from_dyn_object(self.collection.as_ref()?.clone()), "position" => self.position()?.into(), "is_index" => self.is_index().into(), "next" => { @@ -370,7 +373,7 @@ mod taxonomy_object { .or_else(|| self.position().map(|i| i.saturating_add(1)))?; let next = collection.items.get(j)?; - Value::from_any_map_object(next.clone()) + Value::from_dyn_object(next.clone()) }, "previous" => { let collection = self.collection.as_ref()?; @@ -379,183 +382,157 @@ mod taxonomy_object { i => collection.items.get(i - 1)?, }; - Value::from_any_map_object(item.clone()) + Value::from_dyn_object(item.clone()) } - _ => self.item.get_field(name)?, + _ => self.item.get_value(name)?, }; Some(value) } - fn fields(self: &Arc) -> Vec { - let mut item_keys = self.item.fields(); - // TODO: if the item already contains fields with any of the names - // below, we're going to duplicate them. is that okay? - item_keys.extend_from_slice(&[ - intern("site").into(), - intern("collection").into(), - intern("position").into(), - intern("is_index").into(), - intern("next").into(), - intern("previous").into(), - ]); - - item_keys - } + fn enumerate(self: &Arc) -> Enumerator { + self.mapped_enumerator(|this| Box::new({ + let keys = &["site", "collection", "position", "is_index", "next", "previous"]; + let unique_keys = keys.into_iter() + .filter(|x| !this.item.metadata.contains_key(x)) + .map(|x| Value::from(*x)); - fn field_count(self: &Arc) -> usize { - self.item.field_count() + 6 + this.item.metadata.fields().chain(unique_keys) + })) } } - impl MapObject for Site { - fn get_field(self: &Arc, key: &Value) -> Option { + impl Object for Site { + fn get_value(self: &Arc, key: &Value) -> Option { let value = match key.as_str()? { - "items" => Value::from_any_seq_object(self.clone()), - "collections" => Value::from_any_map_object(SiteCollections::new(self.clone())), + "items" => Value::from_dyn_object(SiteItems::new(self.clone())), + "collections" => Value::from_dyn_object(SiteCollections::new(self.clone())), _ => return None, }; Some(value) } - fn static_fields(&self) -> Option<&'static [&'static str]> { - Some(&["items", "collections"]) + fn enumerate(self: &Arc) -> Enumerator { + Enumerator::Str(&["items", "collections"]) } } - impl SeqObject for Site { - fn get_item(self: &Arc, idx: usize) -> Option { - Some(Value::from_any_map_object(self.items.get(idx)?.clone())) + impl Object for SiteItems { + fn get_value(self: &Arc, key: &Value) -> Option { + let value = self.items.get(key.as_usize()?)?; + Some(Value::from_dyn_object(value.clone())) } - fn item_count(self: &Arc) -> usize { - self.items.len() + fn enumerate(self: &Arc) -> Enumerator { + Enumerator::Seq(self.items.len()) } } - impl MapObject for SiteCollections { - fn get_field(self: &Arc, key: &Value) -> Option { - let name = key.as_str()?; - let id = self.collections.keys() - .find(|id| self.tree[**id].relative_path() == Path::new(name))?; - - let collection = self.collections.get(id)?.clone(); - Some(Value::from_any_map_object(collection)) + impl Object for SiteCollections { + fn repr(self: &Arc) -> ObjectRepr { + ObjectRepr::Map } - fn fields(self: &Arc) -> Vec { - self.collections.values() - .map(|c| c.root_entry().relative_path()) - .filter_map(|p| p.to_str()) - .map(Value::from) - .collect() + fn get_value(self: &Arc, key: &Value) -> Option { + let id = self.index.get(key.as_str()?)?; + let collection = self.collections.get(id)?.clone(); + Some(Value::from_dyn_object(collection)) } - fn field_count(self: &Arc) -> usize { - self.collections.len() + fn enumerate(self: &Arc) -> Enumerator { + self.mapped_enumerator(|this| Box::new({ + this.index.keys().map(|k| Value::from(k.clone())) + })) } } - impl MapObject for Collection { - fn get_field(self: &Arc, name: &Value) -> Option { - let value = match name.as_str()? { - "index" => Value::from_any_map_object(self.index.clone()?), - "items" => Value::from_any_seq_object(self.clone()), - "data" => Value::from_any_map_object(CollectionData::new(self.clone())), + impl Object for Collection { + fn get_value(self: &Arc, name: &Value) -> Option { + Some(match name.as_str()? { + "index" => Value::from_dyn_object(self.index.clone()?), + "items" => Value::from_dyn_object(CollectionItems::new(self.clone())), + "data" => Value::from_dyn_object(CollectionData::new(self.clone())), _ => return None, - }; - - Some(value) + }) } - fn static_fields(&self) -> Option<&'static [&'static str]> { - Some(&["index", "items", "data"]) + fn enumerate(self: &Arc) -> Enumerator { + Enumerator::Str(&["index", "items", "data"]) } } - impl SeqObject for Collection { - fn get_item(self: &Arc, idx: usize) -> Option { - Some(Value::from_any_map_object(self.items.get(idx)?.clone())) + impl Object for CollectionItems { + fn get_value(self: &Arc, value: &Value) -> Option { + let item = self.items.get(value.as_usize()?)?; + Some(Value::from_dyn_object(item.clone())) } - fn item_count(self: &Arc) -> usize { - self.items.len() + fn enumerate(self: &Arc) -> Enumerator { + Enumerator::Seq(List::len(&self.items)) } } impl Metadata { - fn get_field(&self, name: &Value) -> Option { + fn get_value(&self, name: &Value) -> Option { self.get_raw(name.as_str()?).map(Value::from) } - fn fields(&self) -> Vec { - self.keys() - .map(Value::from) - .collect() + fn fields(&self) -> impl Iterator + '_ { + self.keys().map(Value::from) } } - impl MapObject for Metadata { - #[inline] - fn get_field(self: &Arc, name: &Value) -> Option { - Metadata::get_field(self, name) + impl Object for Metadata { + fn repr(self: &Arc) -> ObjectRepr { + ObjectRepr::Map } - #[inline(always)] - fn fields(self: &Arc) -> Vec { - Metadata::fields(self) + fn get_value(self: &Arc, name: &Value) -> Option { + Metadata::get_value(self, name) } - fn field_count(self: &Arc) -> usize { - self.len() + fn enumerate(self: &Arc) -> Enumerator { + self.mapped_enumerator(|this| Box::new(this.fields())) } } - impl MapObject for Item { - fn get_field(self: &Arc, name: &Value) -> Option { - match name.as_str() { - Some("id") => Some(self.id.0.into()), - _ => self.metadata.get_field(name) - } + impl Object for Item { + fn repr(self: &Arc) -> ObjectRepr { + ObjectRepr::Map } - fn fields(self: &Arc) -> Vec { - // FIXME: What if `id` already in `self.metadata`? - let mut fields = self.metadata.fields(); - fields.push("id".into()); - fields + fn get_value(self: &Arc, name: &Value) -> Option { + self.metadata.get_value(name) } - fn field_count(self: &Arc) -> usize { - // FIXME: What if `id` already in `self.metadata`? - self.metadata.len() + 1 + fn enumerate(self: &Arc) -> Enumerator { + self.mapped_enumerator(|this| Box::new(this.metadata.fields())) } } - impl MapObject for CollectionData { - fn get_field(self: &Arc, key: &Value) -> Option { + impl Object for CollectionData { + fn repr(self: &Arc) -> ObjectRepr { + ObjectRepr::Map + } + + fn get_value(self: &Arc, key: &Value) -> Option { let name = key.as_str()?; let id = self.data.keys() - .find(|id| self.tree[**id].file_stem() == name)?; + .find(|id| self.entry.tree[**id].file_stem() == name)?; let list = self.data.get(id)?.clone(); - Some(Value::from_any_seq_object(list)) + Some(Value::from_dyn_object(list)) } - fn fields(self: &Arc) -> Vec { - self.data.keys() - .map(|id| self.tree[*id].file_stem()) - .map(intern) - .map(Value::from) - .collect() - } - - fn field_count(self: &Arc) -> usize { - self.data.len() + fn enumerate(self: &Arc) -> Enumerator { + self.mapped_enumerator(|this| Box::new({ + this.data.keys() + .map(|id| this.entry.tree[*id].file_stem()) + .map(Value::from) + })) } } - } impl_error_detail_with_std_error!(minijinja::Error); diff --git a/lib/src/util/variation.rs b/lib/src/util/variation.rs index d1b39da..1f6609e 100644 --- a/lib/src/util/variation.rs +++ b/lib/src/util/variation.rs @@ -5,6 +5,7 @@ pub unsafe trait Variation { #[macro_export] macro_rules! declare_variation { ($v:vis $V:ident of $T:ty) => { + #[derive(Debug)] #[repr(transparent)] $v struct $V(pub $T); diff --git a/lib/src/value/list.rs b/lib/src/value/list.rs index feedd36..fe01319 100644 --- a/lib/src/value/list.rs +++ b/lib/src/value/list.rs @@ -1,9 +1,8 @@ +use core::fmt; + use rayon::prelude::*; use rayon::iter::plumbing::*; -use derive_more::Debug; -#[derive(Debug)] -#[debug("{items:?}")] pub struct List { ordering: parking_lot::RwLock>>, items: boxcar::Vec, @@ -50,6 +49,12 @@ impl List { } } +impl fmt::Debug for List { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_list().entries(self.iter()).finish() + } +} + impl Default for List { fn default() -> Self { Self { diff --git a/lib/src/value/mapper.rs b/lib/src/value/mapper.rs index 53c72bc..c845f83 100644 --- a/lib/src/value/mapper.rs +++ b/lib/src/value/mapper.rs @@ -64,7 +64,7 @@ macro_rules! impl_format { impl_format!(Toml: toml::from_str, toml::de::Error); impl_format!(Json: serde_json::from_str, serde_json::error::Error); -#[derive(derive_more::From, Debug, Default)] +#[derive(Debug, Default)] pub struct Grass { options: grass::Options<'static>, } diff --git a/mockingbird/Cargo.toml b/mockingbird/Cargo.toml index 45dddf7..e9b727b 100644 --- a/mockingbird/Cargo.toml +++ b/mockingbird/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mockingbird" -version = "0.0.1" +version = "0.0.2" edition = "2021" authors = ["Sergio Benitez "] license = "MIT OR Apache-2.0" @@ -11,7 +11,8 @@ readme = "../README.md" [dependencies] serde = { version = "1.0.197", features = ["derive"] } rustc-hash = { version = "1.1" } +xflags = "0.3.2" [dependencies.harper] -version = "0.0.1" +version = "0.0.2" path = "../lib" diff --git a/mockingbird/src/config.rs b/mockingbird/src/config.rs index d8258c4..4b0f999 100644 --- a/mockingbird/src/config.rs +++ b/mockingbird/src/config.rs @@ -5,15 +5,12 @@ use serde::{Deserialize, Serialize}; use harper::url::UrlBuf; use harper::value::{Toml, Format, Value}; -use harper::fstree::{EntryId, FsTree}; +use harper::fstree::FsTree; use harper::error::Result; use harper::templating::{Engine, EngineInit}; #[derive(Debug)] pub struct Config { - pub tree: Arc, - /// The entry `Settings` was read from, if any. - pub entry: Option, pub engine: Arc, pub settings: Settings, } @@ -30,15 +27,15 @@ pub struct Settings { impl Config { pub fn discover(tree: Arc) -> Result { - let (entry, mut settings) = match tree.get(None, crate::CONFIG_FILE) { - Some(entry) => (Some(entry.id), Toml::read(&*entry.path)?), - None => (None, Settings::default()), + let mut settings = match tree.get(None, crate::CONFIG_FILE) { + Some(entry) => Toml::read(&*entry.path)?, + None => Settings::default(), }; settings.root.make_absolute(); settings.aliases.insert("".into(), settings.root.to_string()); let templates_entry = crate::util::dircheck(&tree, None, crate::TEMPLATE_DIR, false)?; let engine = Arc::new(E::init(tree.clone(), templates_entry, &settings)); - Ok(Config { tree, entry, engine, settings }) + Ok(Config { engine, settings }) } } diff --git a/mockingbird/src/discover.rs b/mockingbird/src/discover.rs index 32b37e1..2dc6f8e 100644 --- a/mockingbird/src/discover.rs +++ b/mockingbird/src/discover.rs @@ -1,11 +1,11 @@ use std::sync::Arc; use std::path::{Path, PathBuf}; +use harper::{err, Collection, Site}; use harper::fstree::{EntryId, FsTree}; use harper::templating::EngineInit; use harper::error::Result; use harper::templating::minijinja::MiniJinjaEngine; -use harper::{err, Collection, Site}; use crate::{ASSETS_DIR, CONTENT_DIR, TEMPLATE_DIR, PermaPath}; use crate::config::Config; @@ -79,12 +79,18 @@ impl Mockingbird { // Find all collections, as identified by the presence of an index file. for index in index_files { let group_dir = &self.tree[index.parent.unwrap()]; - let collection = site.get_or_insert_collection(group_dir.id); + let collection = site.get_or_insert_collection(|| { + group_dir.path_relative_to(content_root) + .unwrap() + .to_string_lossy() + .into() + }, group_dir.id); + if let Some(ref existing) = collection.index { return err!( "found multiple index files for a single collection", "faulting collection", group_dir.path.display(), - "first index" => existing.tree[existing.id].path.display(), + "first index" => existing.entry.path.display(), "second index" => index.path.display(), ); } @@ -116,12 +122,10 @@ impl Mockingbird { for entry in files { let collection = match self.parent(site, entry.id) { Some(collection) => collection, - None => site.get_or_insert_collection(content_root.id), + None => site.get_or_insert_collection(|| "/".into(), content_root.id), }; - // let known = entry.file_ext().map_or(false, |ext| RECOGNIZED_EXTS.contains(&ext)); - let collection_entry = &tree[collection.root]; - if entry.depth - collection_entry.depth <= 1 { + if entry.depth - collection.entry.depth <= 1 { collection.new_item(entry.id); } else { collection.new_datum(entry.parent.unwrap(), entry.id); diff --git a/mockingbird/src/main.rs b/mockingbird/src/main.rs index 6992a03..f7c5b4e 100644 --- a/mockingbird/src/main.rs +++ b/mockingbird/src/main.rs @@ -1,16 +1,20 @@ -use std::{path::Path, sync::Arc}; +use std::sync::Arc; +use std::path::Path; -use harper::Renderer; +use harper::{Renderer, Site}; +use harper::error::Result; use harper::value::Value; use harper::path_str::PathStr; +use harper::templating::minijinja::MiniJinjaEngine; use harper::url::Url; -use crate::discover::Mockingbird; - +#[macro_use] +mod util; mod config; mod discover; mod render; -mod util; + +use crate::discover::Mockingbird; pub const CONTENT_DIR: &str = "content"; pub const TEMPLATE_DIR: &str = "templates"; @@ -36,30 +40,50 @@ harper::define_meta_key! { pub Snip : "snippet" => Arc, } -pub fn main() { +pub fn run(input: &Path, output: &Path) -> Result> { + let mockingbird = Mockingbird::new::(input, output)?; + let site = Arc::new(mockingbird.discover()?); + mockingbird.render_site(&site)?; + Ok(site) +} + +mod flags { use std::path::PathBuf; - use harper::templating::minijinja::MiniJinjaEngine; - let mut args = std::env::args().skip(1); - let input = PathBuf::from(args.next().expect("")); - let output = PathBuf::from(args.next().expect("")); + xflags::xflags! { + /// Your friendly neighborhood bird. + cmd mockingbird { + /// Build a site. + default cmd build { + /// Directory containing the site sources + required input: PathBuf + /// Where to write the site to + required output: PathBuf + /// quiet: don't emit anything + optional -q,--quiet + } + /// Print the version and exit. + cmd version { } + } + } +} - let start = std::time::SystemTime::now(); +pub fn main() { harper::markdown::SyntaxHighlight::warm_up(); - let result = Mockingbird::new::(input, output) - .and_then(|mockingbird| Ok((mockingbird.discover()?, mockingbird))) - .and_then(|(site, mockingbird)| { - let site = Arc::new(site); - println!("discovery time: {}ms", start.elapsed().unwrap().as_millis()); - let render = std::time::SystemTime::now(); - let result = mockingbird.render_site(&site); - println!("render time: {}ms", render.elapsed().unwrap().as_millis()); - println!("total time: {}ms", start.elapsed().unwrap().as_millis()); - site.visualize(); - result - }); - if let Err(e) = result { - println!("error: {e}"); + match flags::Mockingbird::from_env_or_exit().subcommand { + flags::MockingbirdCmd::Build(args) => { + let site = run(&args.input, &args.output).unwrap_or_else(|e| { + eprintln!("error: {e}"); + std::process::exit(1) + }); + + if !args.quiet { + site.visualize(); + } + } + flags::MockingbirdCmd::Version(_) => { + println!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")); + } } } diff --git a/mockingbird/src/render.rs b/mockingbird/src/render.rs index d643221..fd0efc4 100644 --- a/mockingbird/src/render.rs +++ b/mockingbird/src/render.rs @@ -39,17 +39,17 @@ impl Renderer for Mockingbird { .render(template.as_str(), site, Some(collection), item) .chain_with(|| error! { "failed to render item", - "path" => item.entry().relative_path().display(), + "path" => item.entry.relative_path().display(), "template used" => template.as_str(), })?) }, None => { - let content: Arc = item.entry().try_read()?; + let content: Arc = item.entry.try_read()?; if !harper::util::is_template(&*content) { return output.write(content); } - let name = item.entry().relative_path().to_string_lossy(); + let name = item.entry.relative_path().to_string_lossy(); output.write(self.config.engine .render_raw(Some(&*name), &content, site, Some(collection), item) .chain_with(|| error! { @@ -74,7 +74,7 @@ impl Renderer for Mockingbird { return Ok(()); } - let entry = item.entry(); + let entry = &*item.entry; match entry.file_ext() { Some("md") | Some("mdown") | Some("markdown") => { let engine = self.config.engine.clone(); @@ -111,10 +111,10 @@ impl Renderer for Mockingbird { // Computte the permapath and Url. let content_root = &self.tree[self.content_root]; - let group_perma = collection.root_entry().path_relative_to(content_root).unwrap(); + let group_perma = collection.entry.path_relative_to(content_root).unwrap(); let rendered = entry.file_ext().map_or(false, |e| KNOWN_EXTS.contains(&e)); let slug = item.metadata - .get_or_insert_with(Slug, || item.entry().file_stem().slugify()) + .get_or_insert_with(Slug, || item.entry.file_stem().slugify()) .map_err(|v| v.type_err(Slug, "invalid slug"))?; let (permapath, mut url): (Cow<'_, Path>, _) = match (kind, rendered) { @@ -133,7 +133,7 @@ impl Renderer for Mockingbird { } (Kind::Datum(_), true) => return Ok(()), (_, false) => { - let path = item.entry() + let path = item.entry .path_relative_to(content_root) .unwrap(); @@ -177,7 +177,7 @@ impl Renderer for Mockingbird { fn render_site_item(&self, item: &Item) -> Result<()> { // TODO: Add cache key `?HASH`? - let entry = item.entry(); + let entry = &*item.entry; let permapath = match item.metadata.get(PermaPath) { Some(perma) => perma.map_err(|v| v.type_err(PermaPath, entry.path.display()))?, None => return Ok(()), diff --git a/mockingbird/src/util.rs b/mockingbird/src/util.rs index 35d6991..ac626dd 100644 --- a/mockingbird/src/util.rs +++ b/mockingbird/src/util.rs @@ -53,3 +53,14 @@ pub fn dircheck>( }, } } + +#[macro_export] +macro_rules! time { + ($e:expr) => {{ + let start = std::time::Instant::now(); + let result = $e; + let elapsed = start.elapsed(); + println!("{} took {}ms", stringify!($e), elapsed.as_millis()); + result + }}; +}