From a2fba89738caac83ce24d40b762d6205f2266361 Mon Sep 17 00:00:00 2001 From: Christine Dodrill Date: Sat, 19 Sep 2020 11:33:46 -0400 Subject: TL;DR Rust (#210) * start mara code * better alt text * more mara tests * cleanups * blog: start tl;dr rust post * more words * feature complete * little oopses * oops lol --- src/app.rs | 155 ------------------------------------------------ src/app/markdown.rs | 79 ++++++++++++++++++++++++ src/app/mod.rs | 141 +++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 3 +- src/post/frontmatter.rs | 2 +- src/post/mod.rs | 15 +++-- src/signalboost.rs | 2 +- 7 files changed, 233 insertions(+), 164 deletions(-) delete mode 100644 src/app.rs create mode 100644 src/app/markdown.rs create mode 100644 src/app/mod.rs (limited to 'src') diff --git a/src/app.rs b/src/app.rs deleted file mode 100644 index 5ffca7c..0000000 --- a/src/app.rs +++ /dev/null @@ -1,155 +0,0 @@ -use crate::{post::Post, signalboost::Person}; -use anyhow::Result; -use comrak::{markdown_to_html, ComrakOptions}; -use serde::Deserialize; -use std::{fs, path::PathBuf}; - -#[derive(Clone, Deserialize)] -pub struct Config { - #[serde(rename = "clackSet")] - clack_set: Vec, - signalboost: Vec, - port: u16, - #[serde(rename = "resumeFname")] - resume_fname: PathBuf, -} - -pub fn markdown(inp: &str) -> String { - let mut options = ComrakOptions::default(); - - options.extension.autolink = true; - options.extension.table = true; - options.extension.description_lists = true; - options.extension.superscript = true; - options.extension.strikethrough = true; - options.extension.footnotes = true; - - options.render.unsafe_ = true; - - markdown_to_html(inp, &options) -} - -async fn patrons() -> Result> { - use patreon::*; - let creds: Credentials = envy::prefixed("PATREON_").from_env().unwrap(); - let cli = Client::new(creds); - - match cli.campaign().await { - Ok(camp) => { - let id = camp.data[0].id.clone(); - - match cli.pledges(id).await { - Ok(users) => Ok(Some(users)), - Err(why) => { - log::error!("error getting pledges: {:?}", why); - Ok(None) - } - } - } - Err(why) => { - log::error!("error getting patreon campaign: {:?}", why); - Ok(None) - } - } -} - -pub const ICON: &'static str = "https://christine.website/static/img/avatar.png"; - -pub struct State { - pub cfg: Config, - pub signalboost: Vec, - pub resume: String, - pub blog: Vec, - pub gallery: Vec, - pub talks: Vec, - pub everything: Vec, - pub jf: jsonfeed::Feed, - pub sitemap: Vec, - pub patrons: Option, -} - -pub async fn init(cfg: PathBuf) -> Result { - let cfg: Config = serde_dhall::from_file(cfg).parse()?; - let sb = cfg.signalboost.clone(); - let resume = fs::read_to_string(cfg.resume_fname.clone())?; - let resume: String = markdown(&resume); - let blog = crate::post::load("blog")?; - let gallery = crate::post::load("gallery")?; - let talks = crate::post::load("talks")?; - let mut everything: Vec = vec![]; - - { - let blog = blog.clone(); - let gallery = gallery.clone(); - let talks = talks.clone(); - everything.extend(blog.iter().cloned()); - everything.extend(gallery.iter().cloned()); - everything.extend(talks.iter().cloned()); - }; - - everything.sort(); - everything.reverse(); - - let mut jfb = jsonfeed::Feed::builder() - .title("Christine Dodrill's Blog") - .description("My blog posts and rants about various technology things.") - .author( - jsonfeed::Author::new() - .name("Christine Dodrill") - .url("https://christine.website") - .avatar(ICON), - ) - .feed_url("https://christine.website/blog.json") - .user_comment("This is a JSON feed of my blogposts. For more information read: https://jsonfeed.org/version/1") - .home_page_url("https://christine.website") - .icon(ICON) - .favicon(ICON); - - for post in &everything { - let post = post.clone(); - jfb = jfb.item(post.clone().into()); - } - - let mut sm: Vec = vec![]; - let smw = sitemap::writer::SiteMapWriter::new(&mut sm); - let mut urlwriter = smw.start_urlset()?; - for url in &[ - "https://christine.website/resume", - "https://christine.website/contact", - "https://christine.website/", - "https://christine.website/blog", - "https://christine.website/signalboost", - ] { - urlwriter.url(*url)?; - } - - for post in &everything { - urlwriter.url(format!("https://christine.website/{}", post.link))?; - } - - urlwriter.end()?; - - Ok(State { - cfg: cfg, - signalboost: sb, - resume: resume, - blog: blog, - gallery: gallery, - talks: talks, - everything: everything, - jf: jfb.build(), - sitemap: sm, - patrons: patrons().await?, - }) -} - -#[cfg(test)] -mod tests { - use anyhow::Result; - #[tokio::test] - async fn init() -> Result<()> { - let _ = pretty_env_logger::try_init(); - super::init("./config.dhall".into()).await?; - Ok(()) - } -} diff --git a/src/app/markdown.rs b/src/app/markdown.rs new file mode 100644 index 0000000..fe33a21 --- /dev/null +++ b/src/app/markdown.rs @@ -0,0 +1,79 @@ +use color_eyre::eyre::{Result, WrapErr}; +use comrak::nodes::{Ast, AstNode, NodeValue}; +use comrak::{format_html, parse_document, markdown_to_html, Arena, ComrakOptions}; +use std::cell::RefCell; +use crate::templates::Html; +use url::Url; + +pub fn render(inp: &str) -> Result { + let mut options = ComrakOptions::default(); + + options.extension.autolink = true; + options.extension.table = true; + options.extension.description_lists = true; + options.extension.superscript = true; + options.extension.strikethrough = true; + options.extension.footnotes = true; + + options.render.unsafe_ = true; + + let arena = Arena::new(); + let root = parse_document(&arena, inp, &options); + + iter_nodes(root, &|node| { + let mut data = node.data.borrow_mut(); + match &mut data.value { + &mut NodeValue::Link(ref mut link) => { + let base = Url::parse("https://christine.website/")?; + let u = base.join(std::str::from_utf8(&link.url.clone())?)?; + if u.scheme() != "conversation" { + return Ok(()); + } + let parent = node.parent().unwrap(); + node.detach(); + let mut message = vec![]; + for child in node.children() { + format_html(child, &options, &mut message)?; + } + let message = std::str::from_utf8(&message)?; + let message = markdown_to_html(message, &options); + let mood = without_first(u.path()); + let name = u.host_str().unwrap_or("Mara"); + + let mut html = vec![]; + crate::templates::mara(&mut html, mood, name, Html(message))?; + + let new_node = + arena.alloc(AstNode::new(RefCell::new(Ast::new(NodeValue::HtmlInline(html))))); + parent.append(new_node); + + Ok(()) + } + _ => Ok(()), + } + })?; + + let mut html = vec![]; + format_html(root, &options, &mut html).unwrap(); + + String::from_utf8(html).wrap_err("post is somehow invalid UTF-8") +} + +fn iter_nodes<'a, F>(node: &'a AstNode<'a>, f: &F) -> Result<()> +where + F: Fn(&'a AstNode<'a>) -> Result<()>, +{ + f(node)?; + for c in node.children() { + iter_nodes(c, f)?; + } + Ok(()) +} + +fn without_first(string: &str) -> &str { + string + .char_indices() + .nth(1) + .and_then(|(i, _)| string.get(i..)) + .unwrap_or("") +} diff --git a/src/app/mod.rs b/src/app/mod.rs new file mode 100644 index 0000000..44f05e7 --- /dev/null +++ b/src/app/mod.rs @@ -0,0 +1,141 @@ +use crate::{post::Post, signalboost::Person}; +use color_eyre::eyre::Result; +use serde::Deserialize; +use std::{fs, path::PathBuf}; + +pub mod markdown; + +#[derive(Clone, Deserialize)] +pub struct Config { + #[serde(rename = "clackSet")] + clack_set: Vec, + signalboost: Vec, + port: u16, + #[serde(rename = "resumeFname")] + resume_fname: PathBuf, +} + +async fn patrons() -> Result> { + use patreon::*; + let creds: Credentials = envy::prefixed("PATREON_").from_env().unwrap(); + let cli = Client::new(creds); + + match cli.campaign().await { + Ok(camp) => { + let id = camp.data[0].id.clone(); + + match cli.pledges(id).await { + Ok(users) => Ok(Some(users)), + Err(why) => { + log::error!("error getting pledges: {:?}", why); + Ok(None) + } + } + } + Err(why) => { + log::error!("error getting patreon campaign: {:?}", why); + Ok(None) + } + } +} + +pub const ICON: &'static str = "https://christine.website/static/img/avatar.png"; + +pub struct State { + pub cfg: Config, + pub signalboost: Vec, + pub resume: String, + pub blog: Vec, + pub gallery: Vec, + pub talks: Vec, + pub everything: Vec, + pub jf: jsonfeed::Feed, + pub sitemap: Vec, + pub patrons: Option, +} + +pub async fn init(cfg: PathBuf) -> Result { + let cfg: Config = serde_dhall::from_file(cfg).parse()?; + let sb = cfg.signalboost.clone(); + let resume = fs::read_to_string(cfg.resume_fname.clone())?; + let resume: String = markdown::render(&resume)?; + let blog = crate::post::load("blog")?; + let gallery = crate::post::load("gallery")?; + let talks = crate::post::load("talks")?; + let mut everything: Vec = vec![]; + + { + let blog = blog.clone(); + let gallery = gallery.clone(); + let talks = talks.clone(); + everything.extend(blog.iter().cloned()); + everything.extend(gallery.iter().cloned()); + everything.extend(talks.iter().cloned()); + }; + + everything.sort(); + everything.reverse(); + + let mut jfb = jsonfeed::Feed::builder() + .title("Christine Dodrill's Blog") + .description("My blog posts and rants about various technology things.") + .author( + jsonfeed::Author::new() + .name("Christine Dodrill") + .url("https://christine.website") + .avatar(ICON), + ) + .feed_url("https://christine.website/blog.json") + .user_comment("This is a JSON feed of my blogposts. For more information read: https://jsonfeed.org/version/1") + .home_page_url("https://christine.website") + .icon(ICON) + .favicon(ICON); + + for post in &everything { + let post = post.clone(); + jfb = jfb.item(post.clone().into()); + } + + let mut sm: Vec = vec![]; + let smw = sitemap::writer::SiteMapWriter::new(&mut sm); + let mut urlwriter = smw.start_urlset()?; + for url in &[ + "https://christine.website/resume", + "https://christine.website/contact", + "https://christine.website/", + "https://christine.website/blog", + "https://christine.website/signalboost", + ] { + urlwriter.url(*url)?; + } + + for post in &everything { + urlwriter.url(format!("https://christine.website/{}", post.link))?; + } + + urlwriter.end()?; + + Ok(State { + cfg: cfg, + signalboost: sb, + resume: resume, + blog: blog, + gallery: gallery, + talks: talks, + everything: everything, + jf: jfb.build(), + sitemap: sm, + patrons: patrons().await?, + }) +} + +#[cfg(test)] +mod tests { + use color_eyre::eyre::Result; + #[tokio::test] + async fn init() -> Result<()> { + let _ = pretty_env_logger::try_init(); + super::init("./config.dhall".into()).await?; + Ok(()) + } +} diff --git a/src/main.rs b/src/main.rs index aa5400e..c1e9e1d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use color_eyre::eyre::Result; use hyper::{header::CONTENT_TYPE, Body, Response}; use prometheus::{Encoder, TextEncoder}; use std::sync::Arc; @@ -21,6 +21,7 @@ fn with_state( #[tokio::main] async fn main() -> Result<()> { + color_eyre::install()?; let _ = kankyo::init(); pretty_env_logger::init(); log::info!("starting up commit {}", env!("GITHUB_SHA")); diff --git a/src/post/frontmatter.rs b/src/post/frontmatter.rs index 1cc8032..615f2c5 100644 --- a/src/post/frontmatter.rs +++ b/src/post/frontmatter.rs @@ -1,6 +1,6 @@ /// This code was borrowed from @fasterthanlime. -use anyhow::{Result}; +use color_eyre::eyre::{Result}; use serde::{Serialize, Deserialize}; #[derive(Eq, PartialEq, Deserialize, Default, Debug, Serialize, Clone)] diff --git a/src/post/mod.rs b/src/post/mod.rs index a948017..c0062a4 100644 --- a/src/post/mod.rs +++ b/src/post/mod.rs @@ -1,5 +1,5 @@ -use anyhow::{anyhow, Result}; use chrono::prelude::*; +use color_eyre::eyre::{eyre, Result, WrapErr}; use glob::glob; use std::{cmp::Ordering, fs}; @@ -75,8 +75,10 @@ pub fn load(dir: &str) -> Result> { for path in glob(&format!("{}/*.markdown", dir))?.filter_map(Result::ok) { log::debug!("loading {:?}", path); - let body = fs::read_to_string(path.clone()).expect("things to work"); - let (fm, content_offset) = frontmatter::Data::parse(body.clone().as_str()).expect("stuff to work"); + let body = + fs::read_to_string(path.clone()).wrap_err_with(|| format!("can't read {:?}", path))?; + let (fm, content_offset) = frontmatter::Data::parse(body.clone().as_str()) + .wrap_err_with(|| format!("can't parse frontmatter of {:?}", path))?; let markup = &body[content_offset..]; let date = NaiveDate::parse_from_str(&fm.clone().date, "%Y-%m-%d")?; @@ -84,7 +86,8 @@ pub fn load(dir: &str) -> Result> { front_matter: fm, link: format!("{}/{}", dir, path.file_stem().unwrap().to_str().unwrap()), body: markup.to_string(), - body_html: crate::app::markdown(&markup), + body_html: crate::app::markdown::render(&markup) + .wrap_err_with(|| format!("can't parse markdown for {:?}", path))?, date: { DateTime::::from_utc( NaiveDateTime::new(date, NaiveTime::from_hms(0, 0, 0)), @@ -97,7 +100,7 @@ pub fn load(dir: &str) -> Result> { } if result.len() == 0 { - Err(anyhow!("no posts loaded")) + Err(eyre!("no posts loaded")) } else { result.sort(); result.reverse(); @@ -108,7 +111,7 @@ pub fn load(dir: &str) -> Result> { #[cfg(test)] mod tests { use super::*; - use anyhow::Result; + use color_eyre::eyre::Result; #[test] fn blog() { diff --git a/src/signalboost.rs b/src/signalboost.rs index 079990b..f580d7c 100644 --- a/src/signalboost.rs +++ b/src/signalboost.rs @@ -13,7 +13,7 @@ pub struct Person { #[cfg(test)] mod tests { - use anyhow::Result; + use color_eyre::eyre::Result; #[test] fn load() -> Result<()> { let _people: Vec = serde_dhall::from_file("./signalboost.dhall").parse()?; -- cgit v1.2.3