From 46705d707f144764f90d5f0082c4df63b07f8b1e Mon Sep 17 00:00:00 2001 From: Maciej Jur Date: Wed, 3 Jul 2024 23:34:31 +0200 Subject: [PATCH] refactor --- src/gen/load.rs | 176 ------------------------ src/gen/mod.rs | 32 ----- src/gen/sack.rs | 111 ---------------- src/html/base.rs | 130 ------------------ src/html/home.rs | 12 +- src/html/isodate.rs | 24 ++++ src/html/list.rs | 29 +--- src/html/misc.rs | 8 +- src/html/mod.rs | 242 +++++++++++++++++++++++++++++++-- src/html/page.rs | 45 ------- src/html/post.rs | 57 +++++++- src/html/show.rs | 34 ----- src/html/slideshow.rs | 87 ++++++++++++ src/html/special.rs | 23 ---- src/html/wiki.rs | 51 +++++-- src/main.rs | 195 ++++++--------------------- src/md/matter.rs | 26 ---- src/md/mod.rs | 1 - src/pipeline.rs | 301 ++++++++++++++++++++++++++++++++++++++++++ src/watch.rs | 2 +- 20 files changed, 791 insertions(+), 795 deletions(-) delete mode 100644 src/gen/load.rs delete mode 100644 src/gen/mod.rs delete mode 100644 src/gen/sack.rs delete mode 100644 src/html/base.rs create mode 100644 src/html/isodate.rs delete mode 100644 src/html/page.rs delete mode 100644 src/html/show.rs create mode 100644 src/html/slideshow.rs create mode 100644 src/pipeline.rs diff --git a/src/gen/load.rs b/src/gen/load.rs deleted file mode 100644 index afdf547..0000000 --- a/src/gen/load.rs +++ /dev/null @@ -1,176 +0,0 @@ -use std::collections::HashSet; -use std::fs::{self, File}; -use std::io::Write; - -use camino::{Utf8Path, Utf8PathBuf}; -use glob::glob; -use hayagriva::Library; - -use crate::html::Linkable; - -use super::Sack; - - -/// Marks whether the item should be treated as a content page, converted into a standalone HTML -/// page, or as a bundled asset. -#[derive(Debug)] -pub enum FileItemKind { - /// Convert to `index.html` - Index, - /// Convert to a bundled asset - Bundle, -} - -/// Metadata for a single item consumed by SSG. -#[derive(Debug)] -pub struct FileItem { - /// Kind of an item - pub kind: FileItemKind, - /// Original source file location - pub path: Utf8PathBuf, -} - -/// Marks how the asset should be processed by the SSG -pub enum AssetKind { - /// Data renderable to HTML - Html(Box String>), - /// Bibliographical data - Bibtex(Library), - /// Images - Image, -} - -/// Asset renderable by the SSG -pub struct Asset { - /// Kind of a processed asset - pub kind: AssetKind, - /// File metadata - pub meta: FileItem, -} - -/// Dynamically generated asset not related to any disk file. -pub struct Dynamic(pub Box String>); - -impl Dynamic { - pub fn new(call: impl Fn(&Sack) -> String + 'static) -> Self { - Self(Box::new(call)) - } -} - -pub enum OutputKind { - Real(Asset), - Fake(Dynamic), -} - -impl From for OutputKind { - fn from(value: Asset) -> Self { - OutputKind::Real(value) - } -} - -impl From for OutputKind { - fn from(value: Dynamic) -> Self { - OutputKind::Fake(value) - } -} - -/// Renderable output -pub struct Output { - pub kind: OutputKind, - pub path: Utf8PathBuf, - /// Optional link to outputted page. - pub link: Option, -} - -/// Variants used for filtering static assets. -pub enum PipelineItem { - /// Unclaimed file, unrecognized file extensions. - Skip(FileItem), - /// Data ready to be processed by SSG. - Take(Output), -} - -impl From for PipelineItem { - fn from(value: FileItem) -> Self { - Self::Skip(value) - } -} - -impl From for PipelineItem { - fn from(value: Output) -> Self { - Self::Take(value) - } -} - - -pub fn gather(pattern: &str, exts: &HashSet<&'static str>) -> Vec { - glob(pattern) - .expect("Invalid glob pattern") - .filter_map(|path| { - let path = path.unwrap(); - let path = Utf8PathBuf::from_path_buf(path).expect("Filename is not valid UTF8"); - - match path.is_dir() { - true => None, - false => Some(to_source(path, exts)) - } - }) - .map(Into::into) - .collect() -} - - -fn to_source(path: Utf8PathBuf, exts: &HashSet<&'static str>) -> FileItem { - let hit = path.extension().map_or(false, |ext| exts.contains(ext)); - - let kind = match hit { - true => FileItemKind::Index, - false => FileItemKind::Bundle, - }; - - FileItem { - kind, - path, - } -} - - -pub fn render_all(items: &[Output]) { - for item in items { - let file = match &item.kind { - OutputKind::Real(a) => Some(&a.meta.path), - OutputKind::Fake(_) => None, - }; - render(item, &Sack::new(items, &item.path, file)); - } -} - -fn render(item: &Output, sack: &Sack) { - let o = Utf8Path::new("dist").join(&item.path); - fs::create_dir_all(o.parent().unwrap()).unwrap(); - - match item.kind { - OutputKind::Real(ref real) => { - let i = &real.meta.path; - - match &real.kind { - AssetKind::Html(closure) => { - let mut file = File::create(&o).unwrap(); - file.write_all(closure(sack).as_bytes()).unwrap(); - println!("HTML: {} -> {}", i, o); - }, - AssetKind::Bibtex(_) => { }, - AssetKind::Image => { - fs::create_dir_all(o.parent().unwrap()).unwrap(); - fs::copy(i, &o).unwrap(); - println!("Image: {} -> {}", i, o); - }, - }; - }, - OutputKind::Fake(Dynamic(ref closure)) => { - let mut file = File::create(&o).unwrap(); - file.write_all(closure(sack).as_bytes()).unwrap(); - println!("Virtual: -> {}", o); - }, - } -} diff --git a/src/gen/mod.rs b/src/gen/mod.rs deleted file mode 100644 index 785a593..0000000 --- a/src/gen/mod.rs +++ /dev/null @@ -1,32 +0,0 @@ -mod load; -mod sack; - -use camino::Utf8PathBuf; -use hayagriva::Library; -use hypertext::Renderable; - -pub use load::{gather, render_all, FileItem, FileItemKind, Asset, AssetKind, PipelineItem, Dynamic, Output}; -pub use sack::{TreePage, Sack}; - -use crate::{html::Linkable, text::md::Outline}; - - -/// Represents a piece of content that can be rendered as a page. -pub trait Content { - fn transform<'f, 'm, 's, 'html, T>( - &'f self, - content: T, - outline: Outline, - sack: &'s Sack, - bib: Option>, - ) -> impl Renderable + 'html - where - 'f: 'html, - 'm: 'html, - 's: 'html, - T: Renderable + 'm; - - fn as_link(&self, path: Utf8PathBuf) -> Option; - - fn render(data: &str, lib: Option<&Library>) -> (Outline, String, Option>); -} diff --git a/src/gen/sack.rs b/src/gen/sack.rs deleted file mode 100644 index 01b7bca..0000000 --- a/src/gen/sack.rs +++ /dev/null @@ -1,111 +0,0 @@ -use std::collections::HashMap; - -use camino::{Utf8Path, Utf8PathBuf}; -use hayagriva::Library; - -use crate::html::{Link, LinkDate, Linkable}; - -use super::{load::{Output, OutputKind}, AssetKind}; - - -#[derive(Debug)] -pub struct TreePage { - pub link: Option, - pub subs: HashMap, -} - -impl TreePage { - fn new() -> Self { - TreePage { - link: None, - subs: HashMap::new(), - } - } - - fn add_link(&mut self, link: &Link) { - let mut ptr = self; - for part in link.path.iter().skip(1) { - ptr = ptr.subs - .entry(part.to_string()) - .or_insert(TreePage::new()); - } - ptr.link = Some(link.clone()); - } -} - - -/// This struct allows for querying the website hierarchy. Separate instance of this struct is -/// passed to each closure contained by some rendered assets. -pub struct Sack<'a> { - /// Literally everything - hole: &'a [Output], - /// Current path for page - path: &'a Utf8PathBuf, - /// Original file location - file: Option<&'a Utf8PathBuf>, -} - -impl<'a> Sack<'a> { - pub fn new(hole: &'a [Output], path: &'a Utf8PathBuf, file: Option<&'a Utf8PathBuf>) -> Self { - Self { hole, path, file } - } - - pub fn get_links(&self, path: &str) -> Vec { - let pattern = glob::Pattern::new(path).expect("Bad glob pattern"); - self.hole.iter() - .filter(|item| pattern.matches_path(item.path.as_ref())) - .filter_map(|item| match &item.link { - Some(Linkable::Date(link)) => Some(link.clone()), - _ => None, - }) - .collect() - } - - pub fn get_tree(&self, path: &str) -> TreePage { - let glob = glob::Pattern::new(path).expect("Bad glob pattern"); - let list = self.hole.iter() - .filter(|item| glob.matches_path(item.path.as_ref())) - .filter_map(|item| match &item.link { - Some(Linkable::Link(link)) => Some(link.clone()), - _ => None, - }); - - let mut tree = TreePage::new(); - for link in list { - tree.add_link(&link); - }; - - tree - } - - pub fn get_library(&self) -> Option<&Library> { - let glob = format!("{}/*.bib", self.path.parent()?); - let glob = glob::Pattern::new(&glob).expect("Bad glob pattern"); - let opts = glob::MatchOptions { - case_sensitive: true, - require_literal_separator: true, - require_literal_leading_dot: false, - }; - - self.hole.iter() - .filter(|item| glob.matches_path_with(item.path.as_ref(), opts)) - .filter_map(|asset| match asset.kind { - OutputKind::Real(ref real) => Some(real), - _ => None, - }) - .find_map(|asset| match asset.kind { - AssetKind::Bibtex(ref lib) => Some(lib), - _ => None, - }) - } - - /// Get the path for output - pub fn get_path(&self) -> &'a Utf8Path { - self.path.as_path() - } - - /// Get the path for original file location - pub fn get_file(&self) -> Option<&'a Utf8Path> { - self.file.map(Utf8PathBuf::as_ref) - } -} diff --git a/src/html/base.rs b/src/html/base.rs deleted file mode 100644 index 0a1f4fa..0000000 --- a/src/html/base.rs +++ /dev/null @@ -1,130 +0,0 @@ -use camino::Utf8Path; -use hypertext::{html_elements, maud, maud_move, GlobalAttributes, Raw, Renderable}; - -use crate::REPO; - - -const JS_RELOAD: &str = r#" -const socket = new WebSocket("ws://localhost:1337"); -socket.addEventListener("message", (event) => { - console.log(event); - window.location.reload(); -}); -"#; - -const JS_IMPORTS: &str = r#" -{ - "imports": { - "reveal": "/js/vanilla/reveal.js", - "photos": "/js/vanilla/photos.js" - } -} -"#; - - -pub fn head(title: &str) -> impl Renderable + '_ { - let title = format!("{} | kamoshi.org", title); - - maud_move!( - meta charset="utf-8"; - meta name="viewport" content="width=device-width, initial-scale=1"; - title { - (title) - } - - // link rel="sitemap" href="/sitemap.xml"; - - link rel="stylesheet" href="/styles.css"; - link rel="stylesheet" href="/static/css/reveal.css"; - link rel="stylesheet" href="/static/css/leaflet.css"; - link rel="stylesheet" href="/static/css/MarkerCluster.css"; - link rel="stylesheet" href="/static/css/MarkerCluster.Default.css"; - link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"; - link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"; - link rel="icon" href="/favicon.ico" sizes="any"; - - script type="importmap" {(Raw(JS_IMPORTS))} - - script { (Raw(JS_RELOAD)) } - ) -} - -pub fn navbar() -> impl Renderable { - static ITEMS: &[(&str, &str)] = &[ - ("Posts", "/posts/"), - ("Slides", "/slides/"), - ("Wiki", "/wiki/"), - ("Map", "/map/"), - ("About", "/about/"), - ("Search", "/search/"), - ]; - - maud!( - nav .p-nav { - input #p-nav-toggle type="checkbox" hidden; - - div .p-nav__bar { - a .p-nav__logo href="/" { - img .p-nav__logo-icon height="48px" width="51px" src="/static/svg/aya.svg" alt=""; - div .p-nav__logo-text { - div .p-nav__logo-main { - (Raw(include_str!("logotype.svg"))) - } - div #p-nav-splash .p-nav__logo-sub { - "夢現の遥か彼方" - } - } - } - - label .p-nav__burger for="p-nav-toggle" tabindex="0" { - span .p-nav__burger-icon {} - } - } - - menu .p-nav__menu { - @for (name, url) in ITEMS { - li .p-nav__menu-item { - a .p-nav__menu-link href=(*url) { - (*name) - } - } - } - } - } - ) -} - -pub fn footer(path: Option<&Utf8Path>) -> impl Renderable { - let copy = format!("Copyright © {} Maciej Jur", &REPO.year); - let mail = "maciej@kamoshi.org"; - let href = format!("mailto:{}", mail); - let link = Utf8Path::new(&REPO.link).join("src/commit").join(&REPO.hash); - let link = match path { - Some(path) => link.join(path), - None => link, - }; - - maud_move!( - footer .footer { - div .left { - div { - (Raw(copy)) - } - a href=(href) { - (mail) - } - } - div .repo { - a href=(link.as_str()) { - (&REPO.hash) - } - div { - (&REPO.date) - } - } - a .right.footer__cc-wrap rel="license" href="http://creativecommons.org/licenses/by/4.0/" { - img .footer__cc-stamp alt="Creative Commons License" width="88" height="31" src="/static/svg/by.svg"; - } - } - ) -} diff --git a/src/html/home.rs b/src/html/home.rs index 2b96c06..edd602f 100644 --- a/src/html/home.rs +++ b/src/html/home.rs @@ -2,9 +2,6 @@ use hypertext::{html_elements, maud, maud_move, GlobalAttributes, Raw, Renderabl use crate::text::md::parse; -use super::page; - - const INTRO: &str = r#" ## かもし @@ -17,7 +14,6 @@ const INTRO: &str = r#" 質問があったらメールを送信してくれてください。 "#; - fn intro() -> impl Renderable { let (_, html, _) = parse(INTRO, None); maud!( @@ -56,9 +52,9 @@ fn photo() -> impl Renderable { } pub fn home<'data, 'home, R>(main: R) -> impl Renderable + 'home - where - 'data: 'home, - R: Renderable + 'data +where + 'data: 'home, + R: Renderable + 'data, { let main = maud_move!( main .l-home { @@ -73,5 +69,5 @@ pub fn home<'data, 'home, R>(main: R) -> impl Renderable + 'home } ); - page("Home", main, None) + crate::html::page("Home", main, None) } diff --git a/src/html/isodate.rs b/src/html/isodate.rs new file mode 100644 index 0000000..e3d3812 --- /dev/null +++ b/src/html/isodate.rs @@ -0,0 +1,24 @@ +//! This module is supplementary to Serde, it allows you tu parse JS dates. + +use chrono::{DateTime, Utc}; +use serde::{self, Deserialize, Deserializer}; + +// pub fn serialize( +// date: &DateTime, +// serializer: S, +// ) -> Result +// where +// S: Serializer, +// { +// let s = date.to_rfc3339(); +// serializer.serialize_str(&s) +// } + +pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + let dt = chrono::DateTime::parse_from_rfc3339(&s).map_err(serde::de::Error::custom)?; + Ok(dt.into()) +} diff --git a/src/html/list.rs b/src/html/list.rs index b965082..1536d02 100644 --- a/src/html/list.rs +++ b/src/html/list.rs @@ -1,35 +1,14 @@ +use crate::{html::page, LinkDate}; use camino::Utf8PathBuf; use chrono::{DateTime, Utc}; use hypertext::{html_elements, maud_move, GlobalAttributes, Renderable}; -use crate::html::page; - - -#[derive(Debug, Clone)] -pub struct Link { - pub path: Utf8PathBuf, - pub name: String, - pub desc: Option, -} - -#[derive(Debug, Clone)] -pub struct LinkDate { - pub link: Link, - pub date: DateTime, -} - -#[derive(Debug, Clone)] -pub enum Linkable { - Link(Link), - Date(LinkDate), -} - pub fn list<'data, 'list>( title: &'data str, - groups: &'data [(i32, Vec)] + groups: &'data [(i32, Vec)], ) -> impl Renderable + 'list - where - 'data: 'list +where + 'data: 'list, { let list = maud_move!( main .page-list-main { diff --git a/src/html/misc.rs b/src/html/misc.rs index 3fba7d3..f0dd44b 100644 --- a/src/html/misc.rs +++ b/src/html/misc.rs @@ -1,11 +1,11 @@ use hypertext::{html_elements, maud_move, GlobalAttributes, Raw, Renderable}; -use crate::gen::{Sack, TreePage}; +use crate::pipeline::{Sack, TreePage}; use crate::text::md::Outline; /// Render the outline for a document -pub fn show_outline(outline: Outline) -> impl Renderable { +pub(crate) fn show_outline(outline: Outline) -> impl Renderable { maud_move!( section .link-tree { h2 .link-tree__heading { @@ -27,7 +27,7 @@ pub fn show_outline(outline: Outline) -> impl Renderable { } /// Render the bibliography for a document -pub fn show_bibliography(bib: Vec) -> impl Renderable { +pub(crate) fn show_bibliography(bib: Vec) -> impl Renderable { maud_move!( section .markdown { h2 { @@ -45,7 +45,7 @@ pub fn show_bibliography(bib: Vec) -> impl Renderable { } /// Render the page tree -pub fn show_page_tree(sack: &Sack, glob: &str) -> impl Renderable { +pub(crate) fn show_page_tree(sack: &Sack, glob: &str) -> impl Renderable { let tree = sack.get_tree(glob); maud_move!( diff --git a/src/html/mod.rs b/src/html/mod.rs index 49b3756..d64cfc9 100644 --- a/src/html/mod.rs +++ b/src/html/mod.rs @@ -1,19 +1,235 @@ -mod base; mod home; -mod page; -mod post; +mod isodate; mod list; -mod show; +mod misc; +mod post; +mod slideshow; mod special; mod wiki; -mod misc; -pub use home::home; -pub use page::page; -pub use post::post; -pub use list::list; -pub use show::show; -pub use special::{map, search}; -pub use wiki::wiki; +use std::collections::HashMap; -pub use list::{Linkable, Link, LinkDate}; +use camino::Utf8Path; +use chrono::Datelike; +use hypertext::{html_elements, maud, maud_move, GlobalAttributes, Raw, Renderable}; + +use crate::REPO; + +pub(crate) use home::home; +pub(crate) use post::Post; +pub(crate) use slideshow::Slideshow; +pub(crate) use wiki::Wiki; + +const JS_RELOAD: &str = r#" +const socket = new WebSocket("ws://localhost:1337"); +socket.addEventListener("message", (event) => { + console.log(event); + window.location.reload(); +}); +"#; + +const JS_IMPORTS: &str = r#" +{ + "imports": { + "reveal": "/js/vanilla/reveal.js", + "photos": "/js/vanilla/photos.js" + } +} +"#; + +fn head(title: &str) -> impl Renderable + '_ { + let title = format!("{} | kamoshi.org", title); + + maud_move!( + meta charset="utf-8"; + meta name="viewport" content="width=device-width, initial-scale=1"; + title { + (title) + } + + // link rel="sitemap" href="/sitemap.xml"; + + link rel="stylesheet" href="/styles.css"; + link rel="stylesheet" href="/static/css/reveal.css"; + link rel="stylesheet" href="/static/css/leaflet.css"; + link rel="stylesheet" href="/static/css/MarkerCluster.css"; + link rel="stylesheet" href="/static/css/MarkerCluster.Default.css"; + link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"; + link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"; + link rel="icon" href="/favicon.ico" sizes="any"; + + script type="importmap" {(Raw(JS_IMPORTS))} + + script { (Raw(JS_RELOAD)) } + ) +} + +fn navbar() -> impl Renderable { + static ITEMS: &[(&str, &str)] = &[ + ("Posts", "/posts/"), + ("Slides", "/slides/"), + ("Wiki", "/wiki/"), + ("Map", "/map/"), + ("About", "/about/"), + ("Search", "/search/"), + ]; + + maud!( + nav .p-nav { + input #p-nav-toggle type="checkbox" hidden; + + div .p-nav__bar { + a .p-nav__logo href="/" { + img .p-nav__logo-icon height="48px" width="51px" src="/static/svg/aya.svg" alt=""; + div .p-nav__logo-text { + div .p-nav__logo-main { + (Raw(include_str!("logotype.svg"))) + } + div #p-nav-splash .p-nav__logo-sub { + "夢現の遥か彼方" + } + } + } + + label .p-nav__burger for="p-nav-toggle" tabindex="0" { + span .p-nav__burger-icon {} + } + } + + menu .p-nav__menu { + @for (name, url) in ITEMS { + li .p-nav__menu-item { + a .p-nav__menu-link href=(*url) { + (*name) + } + } + } + } + } + ) +} + +pub fn footer(path: Option<&Utf8Path>) -> impl Renderable { + let copy = format!("Copyright © {} Maciej Jur", &REPO.year); + let mail = "maciej@kamoshi.org"; + let href = format!("mailto:{}", mail); + let link = Utf8Path::new(&REPO.link) + .join("src/commit") + .join(&REPO.hash); + let link = match path { + Some(path) => link.join(path), + None => link, + }; + + maud_move!( + footer .footer { + div .left { + div { + (Raw(copy)) + } + a href=(href) { + (mail) + } + } + div .repo { + a href=(link.as_str()) { + (&REPO.hash) + } + div { + (&REPO.date) + } + } + a .right.footer__cc-wrap rel="license" href="http://creativecommons.org/licenses/by/4.0/" { + img .footer__cc-stamp alt="Creative Commons License" width="88" height="31" src="/static/svg/by.svg"; + } + } + ) +} + +fn bare<'data, 'html, R>(title: &'data str, main: R) -> impl Renderable + 'html + where + 'data : 'html, + R: Renderable + 'data +{ + maud_move!( + (Raw("")) + html lang="en" { + (head(title)) + + body { + (main) + } + } + ) +} + +fn page<'data, 'main, 'html, T>( + title: &'data str, + main: T, + path: Option<&'data Utf8Path>, +) -> impl Renderable + 'html + where + 'main : 'html, + 'data : 'html, + T: Renderable + 'main +{ + maud_move!( + (Raw("")) + html lang="en" { + (head(title)) + + body { + (navbar()) + (main) + (footer(path)) + } + } + ) +} + +pub(crate) fn to_list(list: Vec) -> String { + let mut groups = HashMap::>::new(); + + for page in list { + groups.entry(page.date.year()).or_default().push(page); + } + + let mut groups: Vec<_> = groups + .into_iter() + .map(|(k, mut v)| { + v.sort_by(|a, b| b.date.cmp(&a.date)); + (k, v) + }) + .collect(); + + groups.sort_by(|a, b| b.0.cmp(&a.0)); + + list::list("", &groups).render().into() +} + +pub(crate) fn map() -> impl Renderable { + page( + "Map", + maud!( + main { + div #map style="height: 100%; width: 100%" {} + + script type="module" { + (Raw("import 'photos';")) + } + } + ), + None, + ) +} + +pub(crate) fn search() -> impl Renderable { + page( + "Search", + maud!( + main #app {} + script type="module" src="/js/search/dist/search.js" {} + ), + None, + ) +} diff --git a/src/html/page.rs b/src/html/page.rs deleted file mode 100644 index 088f536..0000000 --- a/src/html/page.rs +++ /dev/null @@ -1,45 +0,0 @@ -use camino::Utf8Path; -use hypertext::{html_elements, maud_move, GlobalAttributes, Raw, Renderable}; -use crate::html::base::{head, navbar, footer}; - - -pub fn bare<'data, 'html, R>(title: &'data str, main: R) -> impl Renderable + 'html - where - 'data : 'html, - R: Renderable + 'data -{ - maud_move!( - (Raw("")) - html lang="en" { - (head(title)) - - body { - (main) - } - } - ) -} - -pub fn page<'data, 'main, 'html, T>( - title: &'data str, - main: T, - path: Option<&'data Utf8Path>, -) -> impl Renderable + 'html - where - 'main : 'html, - 'data : 'html, - T: Renderable + 'main -{ - maud_move!( - (Raw("")) - html lang="en" { - (head(title)) - - body { - (navbar()) - (main) - (footer(path)) - } - } - ) -} diff --git a/src/html/post.rs b/src/html/post.rs index d9b5bc7..29dfa9d 100644 --- a/src/html/post.rs +++ b/src/html/post.rs @@ -1,11 +1,54 @@ +use camino::Utf8PathBuf; +use chrono::{DateTime, Utc}; +use hayagriva::Library; use hypertext::{html_elements, maud_move, GlobalAttributes, Renderable}; +use serde::Deserialize; -use crate::gen::Sack; -use crate::html::misc::{show_bibliography, show_outline}; -use crate::html::page; -use crate::md::Post; +use crate::pipeline::{Content, Sack}; use crate::text::md::Outline; +use crate::{Linkable, LinkDate}; +/// Represents a simple post. +#[derive(Deserialize, Debug, Clone)] +pub(crate) struct Post { + pub(crate) title: String, + #[serde(with = "super::isodate")] + pub(crate) date: DateTime, + pub(crate) desc: Option, +} + +impl Content for Post { + fn parse(data: &str, lib: Option<&Library>) -> (Outline, String, Option>) { + crate::text::md::parse(data, lib) + } + + fn transform<'f, 'm, 's, 'html, T>( + &'f self, + content: T, + outline: Outline, + sack: &'s Sack, + bib: Option>, + ) -> impl Renderable + 'html + where + 'f: 'html, + 'm: 'html, + 's: 'html, + T: Renderable + 'm, + { + post(self, content, outline, bib, sack) + } + + fn as_link(&self, path: Utf8PathBuf) -> Option { + Some(Linkable::Date(LinkDate { + link: crate::Link { + path, + name: self.title.to_owned(), + desc: self.desc.to_owned(), + }, + date: self.date.to_owned(), + })) + } +} pub fn post<'f, 'm, 's, 'html, T>( fm: &'f Post, @@ -31,7 +74,7 @@ pub fn post<'f, 'm, 's, 'html, T>( label .wiki-aside__slider for="wiki-aside-shown" { img .wiki-icon src="/static/svg/double-arrow.svg" width="24" height="24"; } - (show_outline(outline)) + (crate::html::misc::show_outline(outline)) } article .wiki-article /*class:list={classlist)*/ { @@ -43,11 +86,11 @@ pub fn post<'f, 'm, 's, 'html, T>( } @if let Some(bib) = bib { - (show_bibliography(bib)) + (crate::html::misc::show_bibliography(bib)) } } } ); - page(&fm.title, main, sack.get_file()) + crate::html::page(&fm.title, main, sack.get_file()) } diff --git a/src/html/show.rs b/src/html/show.rs deleted file mode 100644 index ee9f24e..0000000 --- a/src/html/show.rs +++ /dev/null @@ -1,34 +0,0 @@ -use hypertext::{html_elements, maud_move, Renderable, GlobalAttributes, Raw}; - -use crate::md::Slide; - -use super::page; - - -pub fn show<'data, 'show>( - fm: &'data Slide, - slides: impl Renderable + 'data -) -> impl Renderable + 'show - where - 'data: 'show -{ - page::bare(&fm.title, maud_move!( - div .reveal { - div .slides { - (slides) - } - } - - script type="module" { - (Raw("import 'reveal';")) - } - - style {r#" - .slides img { - margin-left: auto; - margin-right: auto; - max-height: 60vh; - } - "#} - )) -} diff --git a/src/html/slideshow.rs b/src/html/slideshow.rs new file mode 100644 index 0000000..effae9c --- /dev/null +++ b/src/html/slideshow.rs @@ -0,0 +1,87 @@ +use camino::Utf8PathBuf; +use chrono::{DateTime, Utc}; +use hayagriva::Library; +use hypertext::{html_elements, maud_move, Renderable, GlobalAttributes, Raw}; +use serde::Deserialize; + +use crate::pipeline::{Content, Sack}; +use crate::text::md::Outline; +use crate::{Link, LinkDate, Linkable}; + + +/// Represents a slideshow +#[derive(Deserialize, Debug, Clone)] +pub(crate) struct Slideshow { + pub title: String, + #[serde(with = "super::isodate")] + pub date: DateTime, + pub desc: Option, +} + +impl Content for Slideshow { + fn transform<'f, 'm, 's, 'html, T>( + &'f self, + content: T, + _: Outline, + _: &'s Sack, + _bib: Option>, + ) -> impl Renderable + 'html + where + 'f: 'html, + 'm: 'html, + 's: 'html, + T: Renderable + 'm { + show(self, content) + } + + fn as_link(&self, path: Utf8PathBuf) -> Option { + Some(Linkable::Date(LinkDate { + link: Link { + path, + name: self.title.to_owned(), + desc: self.desc.to_owned(), + }, + date: self.date.to_owned(), + })) + } + + fn parse(data: &str, _: Option<&Library>) -> (Outline, String, Option>) { + let html = data + .split("\n-----\n") + .map(|chunk| chunk.split("\n---\n").map(|s| crate::text::md::parse(s, None)).map(|e| e.1).collect::>()) + .map(|stack| match stack.len() > 1 { + true => format!("
{}
", stack.into_iter().map(|slide| format!("
{slide}
")).collect::()), + false => format!("
{}
", stack[0]) + }) + .collect::(); + (Outline(vec![]), html, None) + } +} + +pub fn show<'data, 'show>( + fm: &'data Slideshow, + slides: impl Renderable + 'data +) -> impl Renderable + 'show + where + 'data: 'show +{ + crate::html::bare(&fm.title, maud_move!( + div .reveal { + div .slides { + (slides) + } + } + + script type="module" { + (Raw("import 'reveal';")) + } + + style {r#" + .slides img { + margin-left: auto; + margin-right: auto; + max-height: 60vh; + } + "#} + )) +} diff --git a/src/html/special.rs b/src/html/special.rs index 7501516..e69de29 100644 --- a/src/html/special.rs +++ b/src/html/special.rs @@ -1,23 +0,0 @@ -use hypertext::{html_elements, maud, GlobalAttributes, Raw, Renderable}; - -use super::page; - - -pub fn map() -> impl Renderable { - page("Map", maud!( - main { - div #map style="height: 100%; width: 100%" {} - - script type="module" { - (Raw("import 'photos';")) - } - } - ), None) -} - -pub fn search() -> impl Renderable { - page("Search", maud!( - main #app {} - script type="module" src="/js/search/dist/search.js" {} - ), None) -} diff --git a/src/html/wiki.rs b/src/html/wiki.rs index 4b52842..f11869e 100644 --- a/src/html/wiki.rs +++ b/src/html/wiki.rs @@ -1,13 +1,48 @@ +use camino::Utf8PathBuf; +use hayagriva::Library; use hypertext::{html_elements, maud_move, GlobalAttributes, Renderable}; +use serde::Deserialize; -use crate::gen::Sack; -use crate::html::misc::show_page_tree; -use crate::html::{misc::show_bibliography, page}; -use crate::md::Wiki; +use crate::pipeline::{Content, Sack}; use crate::text::md::Outline; +use crate::{Link, Linkable}; +/// Represents a wiki page +#[derive(Deserialize, Debug, Clone)] +pub struct Wiki { + pub title: String, +} -pub fn wiki<'data, 'html, 'sack, T>( +impl Content for Wiki { + fn transform<'f, 'm, 's, 'html, T>( + &'f self, + content: T, + outline: Outline, + sack: &'s Sack, + bib: Option>, + ) -> impl Renderable + 'html + where + 'f: 'html, + 'm: 'html, + 's: 'html, + T: Renderable + 'm { + wiki(self, content, outline, sack, bib) + } + + fn as_link(&self, path: Utf8PathBuf) -> Option { + Some(Linkable::Link(Link { + path, + name: self.title.to_owned(), + desc: None, + })) + } + + fn parse(data: &str, lib: Option<&Library>) -> (Outline, String, Option>) { + crate::text::md::parse(data, lib) + } +} + +fn wiki<'data, 'html, 'sack, T>( fm: &'data Wiki, content: T, _: Outline, @@ -33,7 +68,7 @@ pub fn wiki<'data, 'html, 'sack, T>( // Navigation tree section .link-tree { div { - (show_page_tree(sack, "wiki/**/*.html")) + (crate::html::misc::show_page_tree(sack, "wiki/**/*.html")) } } } @@ -47,11 +82,11 @@ pub fn wiki<'data, 'html, 'sack, T>( } @if let Some(bib) = bib { - (show_bibliography(bib)) + (crate::html::misc::show_bibliography(bib)) } } } ); - page(&fm.title, main, sack.get_file()) + crate::html::page(&fm.title, main, sack.get_file()) } diff --git a/src/main.rs b/src/main.rs index 1beedf8..fd86cb7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,31 +1,26 @@ -use std::collections::HashMap; +mod build; +mod html; +mod md; +mod pipeline; +mod text; +mod ts; +mod utils; +mod watch; + use std::fs; use std::process::Command; use camino::{Utf8Path, Utf8PathBuf}; -use chrono::Datelike; +use chrono::{DateTime, Datelike, Utc}; use clap::{Parser, ValueEnum}; -use gen::{Asset, AssetKind, Content, FileItemKind, Output, PipelineItem, Sack}; -use hayagriva::Library; -use html::{Link, LinkDate, Linkable}; +use pipeline::{Asset, AssetKind, Content, FileItemKind, Output, PipelineItem, Sack}; use hypertext::{Raw, Renderable}; use once_cell::sync::Lazy; use serde::Deserialize; -use text::md::Outline; -use crate::gen::Dynamic; +use crate::pipeline::Virtual; use crate::build::build_styles; -mod md; -mod html; -mod ts; -mod gen; -mod utils; -mod text; -mod watch; -mod build; - - #[derive(Parser, Debug, Clone)] struct Args { #[clap(value_enum, index = 1, default_value = "build")] @@ -68,128 +63,26 @@ static REPO: Lazy = Lazy::new(|| { }); -impl Content for md::Post { - fn transform<'f, 'm, 's, 'html, T>( - &'f self, - content: T, - outline: Outline, - sack: &'s Sack, - bib: Option>, - ) -> impl Renderable + 'html - where - 'f: 'html, - 'm: 'html, - 's: 'html, - T: Renderable + 'm { - html::post(self, content, outline, bib, sack) - } - - fn as_link(&self, path: Utf8PathBuf) -> Option { - Some(Linkable::Date(LinkDate { - link: Link { - path, - name: self.title.to_owned(), - desc: self.desc.to_owned(), - }, - date: self.date.to_owned(), - })) - } - - fn render(data: &str, lib: Option<&Library>) -> (Outline, String, Option>) { - text::md::parse(data, lib) - } +#[derive(Debug, Clone)] +pub struct Link { + pub path: Utf8PathBuf, + pub name: String, + pub desc: Option, } -impl Content for md::Slide { - fn transform<'f, 'm, 's, 'html, T>( - &'f self, - content: T, - _: Outline, - _: &'s Sack, - _bib: Option>, - ) -> impl Renderable + 'html - where - 'f: 'html, - 'm: 'html, - 's: 'html, - T: Renderable + 'm { - html::show(self, content) - } - - fn as_link(&self, path: Utf8PathBuf) -> Option { - Some(Linkable::Date(LinkDate { - link: Link { - path, - name: self.title.to_owned(), - desc: self.desc.to_owned(), - }, - date: self.date.to_owned(), - })) - } - - fn render(data: &str, _: Option<&Library>) -> (Outline, String, Option>) { - let html = data - .split("\n-----\n") - .map(|chunk| chunk.split("\n---\n").map(|s| text::md::parse(s, None)).map(|e| e.1).collect::>()) - .map(|stack| match stack.len() > 1 { - true => format!("
{}
", stack.into_iter().map(|slide| format!("
{slide}
")).collect::()), - false => format!("
{}
", stack[0]) - }) - .collect::(); - (Outline(vec![]), html, None) - } +#[derive(Debug, Clone)] +pub struct LinkDate { + pub link: Link, + pub date: DateTime, } -impl Content for md::Wiki { - fn transform<'f, 'm, 's, 'html, T>( - &'f self, - content: T, - outline: Outline, - sack: &'s Sack, - bib: Option>, - ) -> impl Renderable + 'html - where - 'f: 'html, - 'm: 'html, - 's: 'html, - T: Renderable + 'm { - html::wiki(self, content, outline, sack, bib) - } - - fn as_link(&self, path: Utf8PathBuf) -> Option { - Some(Linkable::Link(Link { - path, - name: self.title.to_owned(), - desc: None, - })) - } - - fn render(data: &str, lib: Option<&Library>) -> (Outline, String, Option>) { - text::md::parse(data, lib) - } +#[derive(Debug, Clone)] +pub enum Linkable { + Link(Link), + Date(LinkDate), } -fn to_list(list: Vec) -> String { - let mut groups = HashMap::>::new(); - - for page in list { - groups.entry(page.date.year()).or_default().push(page); - } - - let mut groups: Vec<_> = groups - .into_iter() - .map(|(k, mut v)| { - v.sort_by(|a, b| b.date.cmp(&a.date)); - (k, v) - }) - .collect(); - - groups.sort_by(|a, b| b.0.cmp(&a.0)); - - html::list("", &groups).render().into() -} - fn to_index(item: PipelineItem) -> PipelineItem where T: for<'de> Deserialize<'de> + Content + 'static, @@ -214,13 +107,13 @@ fn to_index(item: PipelineItem) -> PipelineItem let call = move |sack: &Sack| { let lib = sack.get_library(); - let (outline, html, bib) = T::render(&md, lib); + let (outline, html, bib) = T::parse(&md, lib); T::transform(&fm, Raw(html), outline, sack, bib).render().into() }; Output { kind: Asset { - kind: gen::AssetKind::Html(Box::new(call)), + kind: pipeline::AssetKind::Html(Box::new(call)), meta, }.into(), path, @@ -279,18 +172,18 @@ fn build() { fs::create_dir("dist").unwrap(); let assets: Vec = [ - gen::gather("content/about.md", &["md"].into()) + pipeline::gather("content/about.md", &["md"].into()) .into_iter() - .map(to_index:: as fn(PipelineItem) -> PipelineItem), - gen::gather("content/posts/**/*", &["md", "mdx"].into()) + .map(to_index:: as fn(PipelineItem) -> PipelineItem), + pipeline::gather("content/posts/**/*", &["md", "mdx"].into()) .into_iter() - .map(to_index::), - gen::gather("content/slides/**/*", &["md", "lhs"].into()) + .map(to_index::), + pipeline::gather("content/slides/**/*", &["md", "lhs"].into()) .into_iter() - .map(to_index::), - gen::gather("content/wiki/**/*", &["md"].into()) + .map(to_index::), + pipeline::gather("content/wiki/**/*", &["md"].into()) .into_iter() - .map(to_index::), + .map(to_index::), ] .into_iter() .flatten() @@ -308,24 +201,24 @@ fn build() { assets, vec![ Output { - kind: Dynamic::new(|_| html::map().render().to_owned().into()).into(), + kind: Virtual::new(|_| crate::html::map().render().to_owned().into()).into(), path: "map/index.html".into(), link: None, }, Output { - kind: Dynamic::new(|_| html::search().render().to_owned().into()).into(), + kind: Virtual::new(|_| crate::html::search().render().to_owned().into()).into(), path: "search/index.html".into(), link: None, }, Output { kind: Asset { - kind: gen::AssetKind::Html(Box::new(|_| { + kind: pipeline::AssetKind::Html(Box::new(|_| { let data = std::fs::read_to_string("content/index.md").unwrap(); let (_, html, _) = text::md::parse(&data, None); - html::home(Raw(html)).render().to_owned().into() + crate::html::home(Raw(html)).render().to_owned().into() })), - meta: gen::FileItem { - kind: gen::FileItemKind::Index, + meta: pipeline::FileItem { + kind: pipeline::FileItemKind::Index, path: "content/index.md".into() } }.into(), @@ -333,12 +226,12 @@ fn build() { link: None, }, Output { - kind: Dynamic::new(|sack| to_list(sack.get_links("posts/**/*.html"))).into(), + kind: Virtual::new(|sack| crate::html::to_list(sack.get_links("posts/**/*.html"))).into(), path: "posts/index.html".into(), link: None, }, Output { - kind: Dynamic::new(|sack| to_list(sack.get_links("slides/**/*.html"))).into(), + kind: Virtual::new(|sack| crate::html::to_list(sack.get_links("slides/**/*.html"))).into(), path: "slides/index.html".into(), link: None, }, @@ -350,7 +243,7 @@ fn build() { { let now = std::time::Instant::now(); - gen::render_all(&assets); + pipeline::render_all(&assets); println!("Elapsed: {:.2?}", now.elapsed()); } diff --git a/src/md/matter.rs b/src/md/matter.rs index ec2eb2b..6c6434a 100644 --- a/src/md/matter.rs +++ b/src/md/matter.rs @@ -1,33 +1,7 @@ -use chrono::{DateTime, Utc}; use gray_matter::{engine::YAML, Matter}; use serde::Deserialize; -/// Represents a simple post -#[derive(Deserialize, Debug, Clone)] -pub struct Post { - pub title: String, - #[serde(with = "isodate")] - pub date: DateTime, - pub desc: Option, -} - -/// Represents a slideshow -#[derive(Deserialize, Debug, Clone)] -pub struct Slide { - pub title: String, - #[serde(with = "isodate")] - pub date: DateTime, - pub desc: Option, -} - -/// Represents a wiki page -#[derive(Deserialize, Debug, Clone)] -pub struct Wiki { - pub title: String, -} - - pub fn preflight(raw: &str) -> (T, String) where T: for<'de> Deserialize<'de>, diff --git a/src/md/mod.rs b/src/md/mod.rs index 352f2c4..936f793 100644 --- a/src/md/mod.rs +++ b/src/md/mod.rs @@ -1,4 +1,3 @@ mod matter; -pub use matter::{Post, Slide, Wiki}; pub use matter::preflight; diff --git a/src/pipeline.rs b/src/pipeline.rs new file mode 100644 index 0000000..3866b24 --- /dev/null +++ b/src/pipeline.rs @@ -0,0 +1,301 @@ +//! The purpose of this module is to process the data loaded from content files, which involves +//! loading the data from hard drive, and then processing it further depending on the file type. + +use std::collections::{HashMap, HashSet}; +use std::fs::{self, File}; +use std::io::Write; + +use camino::{Utf8Path, Utf8PathBuf}; +use glob::glob; +use hayagriva::Library; +use hypertext::Renderable; + +use crate::text::md::Outline; +use crate::{Link, LinkDate, Linkable}; + +/// Represents a piece of content that can be rendered as a page. This trait needs to be +/// implemented for the front matter associated with some web page as that is what ultimately +/// matters when rendering the page. Each front matter *definition* maps to exactly one kind of +/// rendered page on the website. +pub(crate) trait Content { + /// Parse the document. Pass an optional library for bibliography. + fn parse(document: &str, library: Option<&Library>) -> (Outline, String, Option>); + + fn transform<'fm, 'md, 'sack, 'html, T>( + &'fm self, + content: T, + outline: Outline, + sack: &'sack Sack, + bib: Option>, + ) -> impl Renderable + 'html + where + 'fm: 'html, + 'md: 'html, + 'sack: 'html, + T: Renderable + 'md; + + fn as_link(&self, path: Utf8PathBuf) -> Option; +} + +/// Marks whether the item should be treated as a content page, converted into a standalone HTML +/// page, or as a bundled asset. +#[derive(Debug)] +pub(crate) enum FileItemKind { + /// Marks items converted to `index.html`. + Index, + /// Marks items from bundle. + Bundle, +} + +/// Metadata for a single item consumed by SSG. +#[derive(Debug)] +pub(crate) struct FileItem { + /// The kind of an item from disk. + pub kind: FileItemKind, + /// Original source file location. + pub path: Utf8PathBuf, +} + +/// Marks how the asset should be processed by the SSG. +pub(crate) enum AssetKind { + /// Data renderable to HTML. In order to process the data, a closure should be called. + Html(Box String>), + /// Bibliographical data. + Bibtex(Library), + /// Image. For now they are simply cloned to the `dist` director. + Image, +} + +/// Asset corresponding to a file on disk. +pub(crate) struct Asset { + /// The kind of a processed asset. + pub kind: AssetKind, + /// File metadata + pub meta: FileItem, +} + +/// Dynamically generated asset not corresponding to any file on disk. This is useful when the +/// generated page is not a content page, e.g. page list. +pub(crate) struct Virtual(Box String>); + +impl Virtual { + pub fn new(call: impl Fn(&Sack) -> String + 'static) -> Self { + Self(Box::new(call)) + } +} + +/// The kind of an output item. +pub(crate) enum OutputKind { + /// Marks an output item which corresponds to a file on disk. + Asset(Asset), + /// Marks an output item which doesn't correspond to any file. + Virtual(Virtual), +} + +impl From for OutputKind { + fn from(value: Asset) -> Self { + OutputKind::Asset(value) + } +} + +impl From for OutputKind { + fn from(value: Virtual) -> Self { + OutputKind::Virtual(value) + } +} + +/// Renderable output +pub(crate) struct Output { + /// The kind of an output item + pub(crate) kind: OutputKind, + /// Path for the output in dist + pub(crate) path: Utf8PathBuf, + /// Optional URL data for outputted page. + pub(crate) link: Option, +} + +/// Items currently in the pipeline. In order for an item to be rendered, it needs to be marked as +/// `Take`, which means it needs to have an output location assigned to itself. +pub(crate) enum PipelineItem { + /// Unclaimed file. + Skip(FileItem), + /// Data ready to be processed. + Take(Output), +} + +impl From for PipelineItem { + fn from(value: FileItem) -> Self { + Self::Skip(value) + } +} + +impl From for PipelineItem { + fn from(value: Output) -> Self { + Self::Take(value) + } +} + +/// This struct allows for querying the website hierarchy. It is passed to each rendered website +/// page, so that it can easily access the website metadata. +pub(crate) struct Sack<'a> { + /// Literally all of the content + hole: &'a [Output], + /// Current path for the page being rendered + path: &'a Utf8PathBuf, + /// Original file location for this page + file: Option<&'a Utf8PathBuf>, +} + +impl<'a> Sack<'a> { + pub fn new(hole: &'a [Output], path: &'a Utf8PathBuf, file: Option<&'a Utf8PathBuf>) -> Self { + Self { hole, path, file } + } + + pub fn get_links(&self, path: &str) -> Vec { + let pattern = glob::Pattern::new(path).expect("Bad glob pattern"); + self.hole + .iter() + .filter(|item| pattern.matches_path(item.path.as_ref())) + .filter_map(|item| match &item.link { + Some(Linkable::Date(link)) => Some(link.clone()), + _ => None, + }) + .collect() + } + + pub fn get_tree(&self, path: &str) -> TreePage { + let glob = glob::Pattern::new(path).expect("Bad glob pattern"); + let list = self + .hole + .iter() + .filter(|item| glob.matches_path(item.path.as_ref())) + .filter_map(|item| match &item.link { + Some(Linkable::Link(link)) => Some(link.clone()), + _ => None, + }); + + let mut tree = TreePage::new(); + for link in list { + tree.add_link(&link); + } + + tree + } + + pub fn get_library(&self) -> Option<&Library> { + let glob = format!("{}/*.bib", self.path.parent()?); + let glob = glob::Pattern::new(&glob).expect("Bad glob pattern"); + let opts = glob::MatchOptions { + case_sensitive: true, + require_literal_separator: true, + require_literal_leading_dot: false, + }; + + self.hole + .iter() + .filter(|item| glob.matches_path_with(item.path.as_ref(), opts)) + .filter_map(|asset| match asset.kind { + OutputKind::Asset(ref real) => Some(real), + _ => None, + }) + .find_map(|asset| match asset.kind { + AssetKind::Bibtex(ref lib) => Some(lib), + _ => None, + }) + } + + /// Get the path for original file location + pub fn get_file(&self) -> Option<&'a Utf8Path> { + self.file.map(Utf8PathBuf::as_ref) + } +} + +#[derive(Debug)] +pub(crate) struct TreePage { + pub link: Option, + pub subs: HashMap, +} + +impl TreePage { + fn new() -> Self { + TreePage { + link: None, + subs: HashMap::new(), + } + } + + fn add_link(&mut self, link: &Link) { + let mut ptr = self; + for part in link.path.iter().skip(1) { + ptr = ptr.subs.entry(part.to_string()).or_insert(TreePage::new()); + } + ptr.link = Some(link.clone()); + } +} + +pub fn gather(pattern: &str, exts: &HashSet<&'static str>) -> Vec { + glob(pattern) + .expect("Invalid glob pattern") + .filter_map(|path| { + let path = path.unwrap(); + let path = Utf8PathBuf::from_path_buf(path).expect("Filename is not valid UTF8"); + + match path.is_dir() { + true => None, + false => Some(to_source(path, exts)), + } + }) + .map(Into::into) + .collect() +} + +fn to_source(path: Utf8PathBuf, exts: &HashSet<&'static str>) -> FileItem { + let hit = path.extension().map_or(false, |ext| exts.contains(ext)); + + let kind = match hit { + true => FileItemKind::Index, + false => FileItemKind::Bundle, + }; + + FileItem { kind, path } +} + +pub fn render_all(items: &[Output]) { + for item in items { + let file = match &item.kind { + OutputKind::Asset(a) => Some(&a.meta.path), + OutputKind::Virtual(_) => None, + }; + render(item, &Sack::new(items, &item.path, file)); + } +} + +fn render(item: &Output, sack: &Sack) { + let o = Utf8Path::new("dist").join(&item.path); + fs::create_dir_all(o.parent().unwrap()).unwrap(); + + match item.kind { + OutputKind::Asset(ref real) => { + let i = &real.meta.path; + + match &real.kind { + AssetKind::Html(closure) => { + let mut file = File::create(&o).unwrap(); + file.write_all(closure(sack).as_bytes()).unwrap(); + println!("HTML: {} -> {}", i, o); + } + AssetKind::Bibtex(_) => {} + AssetKind::Image => { + fs::create_dir_all(o.parent().unwrap()).unwrap(); + fs::copy(i, &o).unwrap(); + println!("Image: {} -> {}", i, o); + } + }; + } + OutputKind::Virtual(Virtual(ref closure)) => { + let mut file = File::create(&o).unwrap(); + file.write_all(closure(sack).as_bytes()).unwrap(); + println!("Virtual: -> {}", o); + } + } +} diff --git a/src/watch.rs b/src/watch.rs index 9de7103..484a954 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -72,7 +72,7 @@ pub fn watch() -> Result<()> { let client = Arc::new(Mutex::new(vec![])); let (tx, rx) = std::sync::mpsc::channel(); - let mut debouncer = new_debouncer(Duration::from_secs(1), tx).unwrap(); + let mut debouncer = new_debouncer(Duration::from_secs(2), tx).unwrap(); debouncer .watcher()