refactor
This commit is contained in:
parent
86b81ceec7
commit
46705d707f
176
src/gen/load.rs
176
src/gen/load.rs
|
@ -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<dyn Fn(&Sack) -> 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<dyn Fn(&Sack) -> 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<Asset> for OutputKind {
|
||||
fn from(value: Asset) -> Self {
|
||||
OutputKind::Real(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Dynamic> 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<Linkable>,
|
||||
}
|
||||
|
||||
/// 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<FileItem> for PipelineItem {
|
||||
fn from(value: FileItem) -> Self {
|
||||
Self::Skip(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Output> for PipelineItem {
|
||||
fn from(value: Output) -> Self {
|
||||
Self::Take(value)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn gather(pattern: &str, exts: &HashSet<&'static str>) -> Vec<PipelineItem> {
|
||||
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);
|
||||
},
|
||||
}
|
||||
}
|
|
@ -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<Vec<String>>,
|
||||
) -> impl Renderable + 'html
|
||||
where
|
||||
'f: 'html,
|
||||
'm: 'html,
|
||||
's: 'html,
|
||||
T: Renderable + 'm;
|
||||
|
||||
fn as_link(&self, path: Utf8PathBuf) -> Option<Linkable>;
|
||||
|
||||
fn render(data: &str, lib: Option<&Library>) -> (Outline, String, Option<Vec<String>>);
|
||||
}
|
111
src/gen/sack.rs
111
src/gen/sack.rs
|
@ -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<Link>,
|
||||
pub subs: HashMap<String, TreePage>,
|
||||
}
|
||||
|
||||
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<LinkDate> {
|
||||
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)
|
||||
}
|
||||
}
|
130
src/html/base.rs
130
src/html/base.rs
|
@ -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";
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
|
@ -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
|
||||
where
|
||||
'data: 'home,
|
||||
R: Renderable + 'data
|
||||
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)
|
||||
}
|
||||
|
|
24
src/html/isodate.rs
Normal file
24
src/html/isodate.rs
Normal file
|
@ -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<S>(
|
||||
// date: &DateTime<Utc>,
|
||||
// serializer: S,
|
||||
// ) -> Result<S::Ok, S::Error>
|
||||
// where
|
||||
// S: Serializer,
|
||||
// {
|
||||
// let s = date.to_rfc3339();
|
||||
// serializer.serialize_str(&s)
|
||||
// }
|
||||
|
||||
pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<DateTime<Utc>, 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())
|
||||
}
|
|
@ -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<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LinkDate {
|
||||
pub link: Link,
|
||||
pub date: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Linkable {
|
||||
Link(Link),
|
||||
Date(LinkDate),
|
||||
}
|
||||
|
||||
|
||||
pub fn list<'data, 'list>(
|
||||
title: &'data str,
|
||||
groups: &'data [(i32, Vec<LinkDate>)]
|
||||
groups: &'data [(i32, Vec<LinkDate>)],
|
||||
) -> impl Renderable + 'list
|
||||
where
|
||||
'data: 'list
|
||||
where
|
||||
'data: 'list,
|
||||
{
|
||||
let list = maud_move!(
|
||||
main .page-list-main {
|
||||
|
|
|
@ -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<String>) -> impl Renderable {
|
||||
pub(crate) fn show_bibliography(bib: Vec<String>) -> impl Renderable {
|
||||
maud_move!(
|
||||
section .markdown {
|
||||
h2 {
|
||||
|
@ -45,7 +45,7 @@ pub fn show_bibliography(bib: Vec<String>) -> 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!(
|
||||
|
|
242
src/html/mod.rs
242
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("<!DOCTYPE html>"))
|
||||
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("<!DOCTYPE html>"))
|
||||
html lang="en" {
|
||||
(head(title))
|
||||
|
||||
body {
|
||||
(navbar())
|
||||
(main)
|
||||
(footer(path))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn to_list(list: Vec<crate::LinkDate>) -> String {
|
||||
let mut groups = HashMap::<i32, Vec<_>>::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,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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("<!DOCTYPE html>"))
|
||||
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("<!DOCTYPE html>"))
|
||||
html lang="en" {
|
||||
(head(title))
|
||||
|
||||
body {
|
||||
(navbar())
|
||||
(main)
|
||||
(footer(path))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
|
@ -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<Utc>,
|
||||
pub(crate) desc: Option<String>,
|
||||
}
|
||||
|
||||
impl Content for Post {
|
||||
fn parse(data: &str, lib: Option<&Library>) -> (Outline, String, Option<Vec<String>>) {
|
||||
crate::text::md::parse(data, lib)
|
||||
}
|
||||
|
||||
fn transform<'f, 'm, 's, 'html, T>(
|
||||
&'f self,
|
||||
content: T,
|
||||
outline: Outline,
|
||||
sack: &'s Sack,
|
||||
bib: Option<Vec<String>>,
|
||||
) -> 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<Linkable> {
|
||||
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())
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
"#}
|
||||
))
|
||||
}
|
87
src/html/slideshow.rs
Normal file
87
src/html/slideshow.rs
Normal file
|
@ -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<Utc>,
|
||||
pub desc: Option<String>,
|
||||
}
|
||||
|
||||
impl Content for Slideshow {
|
||||
fn transform<'f, 'm, 's, 'html, T>(
|
||||
&'f self,
|
||||
content: T,
|
||||
_: Outline,
|
||||
_: &'s Sack,
|
||||
_bib: Option<Vec<String>>,
|
||||
) -> impl Renderable + 'html
|
||||
where
|
||||
'f: 'html,
|
||||
'm: 'html,
|
||||
's: 'html,
|
||||
T: Renderable + 'm {
|
||||
show(self, content)
|
||||
}
|
||||
|
||||
fn as_link(&self, path: Utf8PathBuf) -> Option<Linkable> {
|
||||
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<Vec<String>>) {
|
||||
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::<Vec<_>>())
|
||||
.map(|stack| match stack.len() > 1 {
|
||||
true => format!("<section>{}</section>", stack.into_iter().map(|slide| format!("<section>{slide}</section>")).collect::<String>()),
|
||||
false => format!("<section>{}</section>", stack[0])
|
||||
})
|
||||
.collect::<String>();
|
||||
(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;
|
||||
}
|
||||
"#}
|
||||
))
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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<Vec<String>>,
|
||||
) -> 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<Linkable> {
|
||||
Some(Linkable::Link(Link {
|
||||
path,
|
||||
name: self.title.to_owned(),
|
||||
desc: None,
|
||||
}))
|
||||
}
|
||||
|
||||
fn parse(data: &str, lib: Option<&Library>) -> (Outline, String, Option<Vec<String>>) {
|
||||
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())
|
||||
}
|
||||
|
|
195
src/main.rs
195
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<BuildInfo> = 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<Vec<String>>,
|
||||
) -> 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<Linkable> {
|
||||
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<Vec<String>>) {
|
||||
text::md::parse(data, lib)
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Link {
|
||||
pub path: Utf8PathBuf,
|
||||
pub name: String,
|
||||
pub desc: Option<String>,
|
||||
}
|
||||
|
||||
impl Content for md::Slide {
|
||||
fn transform<'f, 'm, 's, 'html, T>(
|
||||
&'f self,
|
||||
content: T,
|
||||
_: Outline,
|
||||
_: &'s Sack,
|
||||
_bib: Option<Vec<String>>,
|
||||
) -> impl Renderable + 'html
|
||||
where
|
||||
'f: 'html,
|
||||
'm: 'html,
|
||||
's: 'html,
|
||||
T: Renderable + 'm {
|
||||
html::show(self, content)
|
||||
}
|
||||
|
||||
fn as_link(&self, path: Utf8PathBuf) -> Option<Linkable> {
|
||||
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<Vec<String>>) {
|
||||
let html = data
|
||||
.split("\n-----\n")
|
||||
.map(|chunk| chunk.split("\n---\n").map(|s| text::md::parse(s, None)).map(|e| e.1).collect::<Vec<_>>())
|
||||
.map(|stack| match stack.len() > 1 {
|
||||
true => format!("<section>{}</section>", stack.into_iter().map(|slide| format!("<section>{slide}</section>")).collect::<String>()),
|
||||
false => format!("<section>{}</section>", stack[0])
|
||||
})
|
||||
.collect::<String>();
|
||||
(Outline(vec![]), html, None)
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LinkDate {
|
||||
pub link: Link,
|
||||
pub date: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl Content for md::Wiki {
|
||||
fn transform<'f, 'm, 's, 'html, T>(
|
||||
&'f self,
|
||||
content: T,
|
||||
outline: Outline,
|
||||
sack: &'s Sack,
|
||||
bib: Option<Vec<String>>,
|
||||
) -> 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<Linkable> {
|
||||
Some(Linkable::Link(Link {
|
||||
path,
|
||||
name: self.title.to_owned(),
|
||||
desc: None,
|
||||
}))
|
||||
}
|
||||
|
||||
fn render(data: &str, lib: Option<&Library>) -> (Outline, String, Option<Vec<String>>) {
|
||||
text::md::parse(data, lib)
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Linkable {
|
||||
Link(Link),
|
||||
Date(LinkDate),
|
||||
}
|
||||
|
||||
|
||||
fn to_list(list: Vec<LinkDate>) -> String {
|
||||
let mut groups = HashMap::<i32, Vec<_>>::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<T>(item: PipelineItem) -> PipelineItem
|
||||
where
|
||||
T: for<'de> Deserialize<'de> + Content + 'static,
|
||||
|
@ -214,13 +107,13 @@ fn to_index<T>(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<Output> = [
|
||||
gen::gather("content/about.md", &["md"].into())
|
||||
pipeline::gather("content/about.md", &["md"].into())
|
||||
.into_iter()
|
||||
.map(to_index::<md::Post> as fn(PipelineItem) -> PipelineItem),
|
||||
gen::gather("content/posts/**/*", &["md", "mdx"].into())
|
||||
.map(to_index::<crate::html::Post> as fn(PipelineItem) -> PipelineItem),
|
||||
pipeline::gather("content/posts/**/*", &["md", "mdx"].into())
|
||||
.into_iter()
|
||||
.map(to_index::<md::Post>),
|
||||
gen::gather("content/slides/**/*", &["md", "lhs"].into())
|
||||
.map(to_index::<crate::html::Post>),
|
||||
pipeline::gather("content/slides/**/*", &["md", "lhs"].into())
|
||||
.into_iter()
|
||||
.map(to_index::<md::Slide>),
|
||||
gen::gather("content/wiki/**/*", &["md"].into())
|
||||
.map(to_index::<crate::html::Slideshow>),
|
||||
pipeline::gather("content/wiki/**/*", &["md"].into())
|
||||
.into_iter()
|
||||
.map(to_index::<md::Wiki>),
|
||||
.map(to_index::<crate::html::Wiki>),
|
||||
]
|
||||
.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());
|
||||
}
|
||||
|
||||
|
|
|
@ -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<Utc>,
|
||||
pub desc: Option<String>,
|
||||
}
|
||||
|
||||
/// Represents a slideshow
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Slide {
|
||||
pub title: String,
|
||||
#[serde(with = "isodate")]
|
||||
pub date: DateTime<Utc>,
|
||||
pub desc: Option<String>,
|
||||
}
|
||||
|
||||
/// Represents a wiki page
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Wiki {
|
||||
pub title: String,
|
||||
}
|
||||
|
||||
|
||||
pub fn preflight<T>(raw: &str) -> (T, String)
|
||||
where
|
||||
T: for<'de> Deserialize<'de>,
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
mod matter;
|
||||
|
||||
pub use matter::{Post, Slide, Wiki};
|
||||
pub use matter::preflight;
|
||||
|
|
301
src/pipeline.rs
Normal file
301
src/pipeline.rs
Normal file
|
@ -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<Vec<String>>);
|
||||
|
||||
fn transform<'fm, 'md, 'sack, 'html, T>(
|
||||
&'fm self,
|
||||
content: T,
|
||||
outline: Outline,
|
||||
sack: &'sack Sack,
|
||||
bib: Option<Vec<String>>,
|
||||
) -> impl Renderable + 'html
|
||||
where
|
||||
'fm: 'html,
|
||||
'md: 'html,
|
||||
'sack: 'html,
|
||||
T: Renderable + 'md;
|
||||
|
||||
fn as_link(&self, path: Utf8PathBuf) -> Option<Linkable>;
|
||||
}
|
||||
|
||||
/// 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<dyn Fn(&Sack) -> 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<dyn Fn(&Sack) -> 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<Asset> for OutputKind {
|
||||
fn from(value: Asset) -> Self {
|
||||
OutputKind::Asset(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Virtual> 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<Linkable>,
|
||||
}
|
||||
|
||||
/// 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<FileItem> for PipelineItem {
|
||||
fn from(value: FileItem) -> Self {
|
||||
Self::Skip(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Output> 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<LinkDate> {
|
||||
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<Link>,
|
||||
pub subs: HashMap<String, TreePage>,
|
||||
}
|
||||
|
||||
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<PipelineItem> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
Loading…
Reference in a new issue