summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/blog/mod.rs12
-rw-r--r--src/blog/post.rs170
-rw-r--r--src/build.rs28
-rw-r--r--src/internal/markdown.rs21
-rw-r--r--src/internal/mod.rs16
-rw-r--r--src/main.rs19
6 files changed, 262 insertions, 4 deletions
diff --git a/src/blog/mod.rs b/src/blog/mod.rs
index 199d6db..3640bcd 100644
--- a/src/blog/mod.rs
+++ b/src/blog/mod.rs
@@ -1,10 +1,18 @@
+pub mod post;
pub mod handlers {
+use std::sync::Arc;
+
use warp::{Reply, Rejection};
use warp::http::Response;
+use crate::templates::{self, Html, RenderRucte};
+
+use crate::internal::SiteState;
- pub async fn list() -> Result<impl Reply, Rejection> {
- Ok("test")
+ pub async fn list(state: Arc<SiteState>) -> Result<impl Reply, Rejection> {
+ let state = state.clone();
+ Response::builder()
+ .html(|o| templates::index_html(o))
}
pub async fn post(name: String) -> Result<impl Reply, Rejection> {
diff --git a/src/blog/post.rs b/src/blog/post.rs
new file mode 100644
index 0000000..abe57aa
--- /dev/null
+++ b/src/blog/post.rs
@@ -0,0 +1,170 @@
+use std::{cmp::Ordering, path::PathBuf};
+use glob::glob;
+use color_eyre::eyre::{Result, Context, eyre};
+use tokio::fs;
+
+use chrono::prelude::*;
+
+
+#[derive(Eq, PartialEq, Debug, Clone)]
+pub struct Post {
+ pub front_matter: frontmatter::Data,
+ pub body_html: String,
+ pub date: DateTime<FixedOffset>,
+}
+
+impl Ord for Post {
+ fn cmp(&self, other: &Self) -> Ordering {
+ self.partial_cmp(&other).unwrap()
+ }
+}
+
+impl PartialOrd for Post {
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+ Some(self.date.cmp(&other.date))
+ }
+}
+
+async fn read_post(dir: &str, fname: PathBuf) -> Result<Post> {
+ let body = fs::read_to_string(fname.clone())
+ .await
+ .wrap_err_with(|| format!("couldn't read {:?}", fname))?;
+
+ let (front_matter, content_offset) = frontmatter::Data::parse(body.clone().as_str())
+ .wrap_err_with(|| format!("can't parse frontmatter of {:?}", fname))?;
+
+ let body = &body[content_offset..];
+ let date = NaiveDate::parse_from_str(&front_matter.clone().date, "%Y-%m-%d")
+ .map_err(|why| eyre!("error parsing date in {:?}: {}", fname, why))?;
+ let link = format!("{}/{}", dir, fname.file_stem().unwrap().to_str().unwrap());
+ let body_html = crate::internal::markdown::render(&body)
+ .wrap_err_with(|| format!("can't parse markdown for {:?}", fname))?;
+ let date: DateTime<FixedOffset> = DateTime::<Utc>::from_utc(NaiveDateTime::new(date, NaiveTime::from_hms(0,0,0)), Utc)
+ .with_timezone(&Utc)
+ .into();
+
+ Ok(Post {
+ front_matter,
+ body_html,
+ date
+ } )
+}
+
+pub async fn load(dir: &str) -> Result<Vec<Post>> {
+ let futs = glob(&format!("{}/*.md", dir))?
+ .filter_map(Result::ok)
+ .map(|fname| read_post(dir, fname));
+
+ Ok(Vec::new())
+}
+
+mod frontmatter {
+ use serde::{Serialize, Deserialize};
+ use color_eyre::eyre::Result;
+ #[derive(Eq, PartialEq, Deserialize, Default, Debug, Serialize, Clone)]
+ pub struct Data {
+ pub title: String,
+ pub date: String,
+ pub series: Option<Vec<String>>,
+ pub tags: Option<Vec<String>>,
+ }
+
+ enum ParseState {
+ Searching,
+ ReadingFM { buf: String, line_start: bool },
+ SkipNewLine { end: bool },
+ ReadingMark { count: usize, end: bool },
+ }
+
+ #[derive(Debug, thiserror::Error)]
+ enum Error {
+ #[error("EOF while parsing")]
+ EOF,
+ #[error("Error parsing YAML: {0:?}")]
+ Yaml(#[from] serde_yaml::Error),
+ }
+
+ impl Data {
+ pub fn parse(input: &str) -> Result<(Data, usize)> {
+ let mut state = ParseState::Searching;
+
+ let mut payload = None;
+ let offset;
+
+ let mut chars = input.char_indices();
+ 'parse: loop {
+ let (i, ch) = match chars.next() {
+ Some(x) => x,
+ None => return Err(Error::EOF)?,
+ };
+ match &mut state {
+ ParseState::Searching => match ch {
+ '-' => {
+ state = ParseState::ReadingMark {
+ count: 1,
+ end: false,
+ };
+ },
+ '\n' | '\t' | ' ' => {
+
+ },
+ _ => {
+ panic!("Start of frontmatter not found!");
+ }
+ },
+ ParseState::ReadingMark {count, end } => match ch {
+ '-' => {
+ *count += 1;
+ if *count == 3 {
+ state = ParseState::SkipNewLine{ end: *end };
+ }
+ }
+ _ => {
+ panic!("Malformed frontmatter");
+ }
+ },
+ ParseState::SkipNewLine { end } => match ch {
+ '\n' => {
+ if *end {
+ offset = i + 1;
+ break 'parse;
+ } else {
+ state = ParseState::ReadingFM {
+ buf: String::new(),
+ line_start: true,
+ };
+ }
+ },
+ _ => {
+ panic!("Expected newline, got {:?}", ch);
+ }
+ },
+ ParseState::ReadingFM { buf, line_start } => match ch {
+ '-' if *line_start => {
+ let mut state_tmp = ParseState::ReadingMark {
+ count: 1,
+ end: true,
+ };
+ std::mem::swap(&mut state, &mut state_tmp);
+ if let ParseState::ReadingFM {buf, ..} = state_tmp {
+ payload = Some(buf);
+ } else {
+ unreachable!();
+ }
+ },
+ ch => {
+ buf.push(ch);
+ *line_start = ch == '\n';
+ }
+ }
+ }
+ }
+
+ let payload = payload.unwrap();
+
+ let fm = serde_yaml::from_str(&payload)?;
+
+ Ok((fm, offset))
+ }
+ }
+}
diff --git a/src/build.rs b/src/build.rs
new file mode 100644
index 0000000..3fcea82
--- /dev/null
+++ b/src/build.rs
@@ -0,0 +1,28 @@
+use std::process::Command;
+use ructe::{Ructe, RucteError};
+
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+ let mut ructe = Ructe::from_env()?;
+ let mut statics = ructe.statics()?;
+ statics.add_files("statics")?;
+ ructe.compile_templates("templates")?;
+
+ let output = Command::new("git")
+ .args(&["rev-parse", "HEAD"])
+ .output()
+ .unwrap();
+
+ let out = std::env::var("out").unwrap_or("/fake".into());
+ println!("cargo:rustc-env=out={}", out);
+
+ let git_hash = String::from_utf8(output.stdout).unwrap();
+ println!(
+ "cargo:rustc-env=GIT_SHA={}",
+ if git_hash.as_str() == "" {
+ out.into()
+ } else {
+ git_hash
+ }
+ );
+ Ok(())
+}
diff --git a/src/internal/markdown.rs b/src/internal/markdown.rs
new file mode 100644
index 0000000..9798876
--- /dev/null
+++ b/src/internal/markdown.rs
@@ -0,0 +1,21 @@
+use color_eyre::{Result, eyre::Context};
+use comrak::{ComrakOptions, Arena, parse_document, format_html};
+
+
+pub fn render(inp: &str) -> Result<String> {
+ let mut options = ComrakOptions::default();
+ options.extension.autolink = true;
+ options.extension.table = true;
+ options.extension.superscript = true;
+ options.extension.footnotes = true;
+
+ options.render.unsafe_ = true;
+
+ let arena = Arena::new();
+ let root = parse_document(&arena, inp, &options);
+
+ let mut html = vec![];
+ format_html(root, &options, &mut html).unwrap();
+
+ String::from_utf8(html).wrap_err("this is somehow not UTF-8")
+}
diff --git a/src/internal/mod.rs b/src/internal/mod.rs
new file mode 100644
index 0000000..f924c50
--- /dev/null
+++ b/src/internal/mod.rs
@@ -0,0 +1,16 @@
+use color_eyre::eyre::Result;
+use crate::blog::post::Post;
+
+pub mod markdown;
+
+pub struct SiteState {
+ pub blog: Vec<Post>,
+}
+
+pub async fn init() -> Result<SiteState> {
+ let blog = crate::blog::post::load("blog").await?;
+
+ Ok(SiteState {
+ blog,
+ })
+}
diff --git a/src/main.rs b/src/main.rs
index 8e92b51..04ab18e 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,20 +1,31 @@
+#[macro_use]
+extern crate tracing;
+
use color_eyre::eyre::Result;
-use std::net::IpAddr;
+use std::{net::IpAddr, sync::Arc};
use warp::Filter;
use std::str::FromStr;
pub mod blog;
+mod internal;
+
+use internal::SiteState;
#[tokio::main]
async fn main() -> Result<()> {
color_eyre::install()?;
+ tracing_subscriber::fmt::init();
+ info!("Starting launch of {}", env!("GIT_SHA"));
+
// Load .env
kankyo::init();
+ let state = Arc::new(internal::init().await?);
+
let blog_base = warp::path!("blog" / ..);
- let blog_list = blog_base.and_then(blog::handlers::list);
+ let blog_list = blog_base.and(give_site_state(state.clone())).and_then(blog::handlers::list);
let blog_post = blog_base.and(
warp::path!(String)
.and(warp::get()).and_then(blog::handlers::post));
@@ -36,3 +47,7 @@ async fn main() -> Result<()> {
Ok(())
}
+
+fn give_site_state(sitestate: Arc<SiteState>) -> impl Filter<Extract = (Arc<SiteState>,), Error=std::convert::Infallible> + Clone {
+ warp::any().map(move || sitestate.clone())
+}