This commit is contained in:
Maciej Jur 2024-07-03 23:34:31 +02:00
parent 86b81ceec7
commit 46705d707f
Signed by: kamov
GPG key ID: 191CBFF5F72ECAFD
20 changed files with 791 additions and 795 deletions

View file

@ -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);
},
}
}

View file

@ -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>>);
}

View file

@ -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)
}
}

View file

@ -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 &copy; {} 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";
}
}
)
}

View file

@ -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)
}

24
src/html/isodate.rs Normal file
View 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())
}

View file

@ -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 {

View file

@ -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!(

View file

@ -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 &copy; {} 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,
)
}

View file

@ -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))
}
}
)
}

View file

@ -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())
}

View 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
View 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;
}
"#}
))
}

View file

@ -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)
}

View file

@ -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())
}

View file

@ -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());
}

View file

@ -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>,

View file

@ -1,4 +1,3 @@
mod matter;
pub use matter::{Post, Slide, Wiki};
pub use matter::preflight;

301
src/pipeline.rs Normal file
View 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);
}
}
}

View file

@ -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()