diff options
| author | Christine Dodrill <me@christine.website> | 2020-07-16 15:32:30 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-07-16 15:32:30 -0400 |
| commit | 385d25c9f96c0acd5d932488e3bd0ed36ceb4dd7 (patch) | |
| tree | af789f7250519b23038a7e5ea0ae7f4f4c1ffdfc /src/post | |
| parent | 449e934246c82d90dd0aac2644d67f928befeeb4 (diff) | |
| download | xesite-385d25c9f96c0acd5d932488e3bd0ed36ceb4dd7.tar.xz xesite-385d25c9f96c0acd5d932488e3bd0ed36ceb4dd7.zip | |
Rewrite site backend in Rust (#178)
* add shell.nix changes for Rust #176
* set up base crate layout
* add first set of dependencies
* start adding basic app modules
* start html templates
* serve index page
* add contact and feeds pages
* add resume rendering support
* resume cleanups
* get signalboost page working
* rewrite config to be in dhall
* more work
* basic generic post loading
* more tests
* initial blog index support
* fix routing?
* render blogposts
* X-Clacks-Overhead
* split blog handlers into blog.rs
* gallery index
* gallery posts
* fix hashtags
* remove instantpage (it messes up the metrics)
* talk support + prometheus
* Create rust.yml
* Update rust.yml
* Update codeql-analysis.yml
* add jsonfeed library
* jsonfeed support
* rss/atom
* go mod tidy
* atom: add posted date
* rss: add publishing date
* nix: build rust program
* rip out go code
* rip out go templates
* prepare for serving in docker
* create kubernetes deployment
* create automagic deployment
* build docker images on non-master
* more fixes
* fix timestamps
* fix RSS/Atom/JSONFeed validation errors
* add go vanity import redirecting
* templates/header: remove this
* atom feed: fixes
* fix?
* fix??
* fix rust tests
* Update rust.yml
* automatically show snow during the winter
* fix dates
* show commit link in footer
* sitemap support
* fix compiler warning
* start basic patreon client
* integrate kankyo
* fix patreon client
* add patrons page
* remove this
* handle patron errors better
* fix build
* clean up deploy
* sort envvars for deploy
* remove deps.nix
* shell.nix: remove go
* update README
* fix envvars for tests
* nice
* blog: add rewrite in rust post
* blog/site-update: more words
Diffstat (limited to 'src/post')
| -rw-r--r-- | src/post/frontmatter.rs | 114 | ||||
| -rw-r--r-- | src/post/mod.rs | 178 |
2 files changed, 292 insertions, 0 deletions
diff --git a/src/post/frontmatter.rs b/src/post/frontmatter.rs new file mode 100644 index 0000000..1cc8032 --- /dev/null +++ b/src/post/frontmatter.rs @@ -0,0 +1,114 @@ +/// This code was borrowed from @fasterthanlime. + +use anyhow::{Result}; +use serde::{Serialize, Deserialize}; + +#[derive(Eq, PartialEq, Deserialize, Default, Debug, Serialize, Clone)] +pub struct Data { + pub title: String, + pub date: String, + pub series: Option<String>, + pub tags: Option<Vec<String>>, + pub slides_link: Option<String>, + pub image: Option<String>, + pub thumb: Option<String>, + pub show: Option<bool>, +} + +enum State { + SearchForStart, + ReadingMarker { count: usize, end: bool }, + ReadingFrontMatter { buf: String, line_start: bool }, + SkipNewline { end: bool }, +} + +#[derive(Debug, thiserror::Error)] +enum Error { + #[error("EOF while parsing frontmatter")] + EOF, + #[error("Error parsing yaml: {0:?}")] + Yaml(#[from] serde_yaml::Error), +} + +impl Data { + pub fn parse(input: &str) -> Result<(Data, usize)> { + let mut state = State::SearchForStart; + + let mut payload = None; + let offset; + + let mut chars = input.char_indices(); + 'parse: loop { + let (idx, ch) = match chars.next() { + Some(x) => x, + None => return Err(Error::EOF)?, + }; + match &mut state { + State::SearchForStart => match ch { + '-' => { + state = State::ReadingMarker { + count: 1, + end: false, + }; + } + '\n' | '\t' | ' ' => { + // ignore whitespace + } + _ => { + panic!("Start of frontmatter not found"); + } + }, + State::ReadingMarker { count, end } => match ch { + '-' => { + *count += 1; + if *count == 3 { + state = State::SkipNewline { end: *end }; + } + } + _ => { + panic!("Malformed frontmatter marker"); + } + }, + State::SkipNewline { end } => match ch { + '\n' => { + if *end { + offset = idx + 1; + break 'parse; + } else { + state = State::ReadingFrontMatter { + buf: String::new(), + line_start: true, + }; + } + } + _ => panic!("Expected newline, got {:?}",), + }, + State::ReadingFrontMatter { buf, line_start } => match ch { + '-' if *line_start => { + let mut state_temp = State::ReadingMarker { + count: 1, + end: true, + }; + std::mem::swap(&mut state, &mut state_temp); + if let State::ReadingFrontMatter { buf, .. } = state_temp { + payload = Some(buf); + } else { + unreachable!(); + } + } + ch => { + buf.push(ch); + *line_start = ch == '\n'; + } + }, + } + } + + // unwrap justification: option set in state machine, Rust can't statically analyze it + let payload = payload.unwrap(); + + let fm: Self = serde_yaml::from_str(&payload)?; + + Ok((fm, offset)) + } +} diff --git a/src/post/mod.rs b/src/post/mod.rs new file mode 100644 index 0000000..ca1f3b6 --- /dev/null +++ b/src/post/mod.rs @@ -0,0 +1,178 @@ +use anyhow::{anyhow, Result}; +use atom_syndication as atom; +use chrono::prelude::*; +use glob::glob; +use std::{cmp::Ordering, fs}; + +pub mod frontmatter; + +#[derive(Eq, PartialEq, Debug, Clone)] +pub struct Post { + pub front_matter: frontmatter::Data, + pub link: String, + pub body: String, + pub body_html: String, + pub date: DateTime<FixedOffset>, +} + +impl Into<jsonfeed::Item> for Post { + fn into(self) -> jsonfeed::Item { + let mut result = jsonfeed::Item::builder() + .title(self.front_matter.title) + .content_html(self.body_html) + .content_text(self.body) + .id(format!("https://christine.website/{}", self.link)) + .url(format!("https://christine.website/{}", self.link)) + .date_published(self.date.to_rfc3339()) + .author( + jsonfeed::Author::new() + .name("Christine Dodrill") + .url("https://christine.website") + .avatar("https://christine.website/static/img/avatar.png"), + ); + + let mut tags: Vec<String> = vec![]; + + if let Some(series) = self.front_matter.series { + tags.push(series); + } + + if let Some(mut meta_tags) = self.front_matter.tags { + tags.append(&mut meta_tags); + } + + if tags.len() != 0 { + result = result.tags(tags); + } + + if let Some(image_url) = self.front_matter.image { + result = result.image(image_url); + } + + result.build().unwrap() + } +} + +impl Into<atom::Entry> for Post { + fn into(self) -> atom::Entry { + let mut content = atom::ContentBuilder::default(); + + content.src(format!("https://christine.website/{}", self.link)); + content.content_type(Some("text/html;charset=utf-8".into())); + content.value(Some(xml::escape::escape_str_pcdata(&self.body_html).into())); + + let content = content.build().unwrap(); + + let mut result = atom::EntryBuilder::default(); + result.id(format!("https://christine.website/{}", self.link)); + result.contributors({ + let mut me = atom::Person::default(); + + me.set_name("Christine Dodrill"); + me.set_email("me@christine.website".to_string()); + me.set_uri("https://christine.website".to_string()); + + vec![me] + }); + result.title(self.front_matter.title); + let mut link = atom::Link::default(); + link.href = format!("https://christine.website/{}", self.link); + result.links(vec![link]); + result.content(content); + result.published(self.date); + + result.build().unwrap() + } +} + +impl Into<rss::Item> for Post { + fn into(self) -> rss::Item { + let mut guid = rss::Guid::default(); + guid.set_value(format!("https://christine.website/{}", self.link)); + let mut result = rss::ItemBuilder::default(); + result.title(Some(self.front_matter.title)); + result.link(format!("https://christine.website/{}", self.link)); + result.guid(guid); + result.author(Some("me@christine.website (Christine Dodrill)".to_string())); + result.content(self.body_html); + result.pub_date(self.date.to_rfc2822()); + + result.build().unwrap() + } +} + +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)) + } +} + +impl Post { + pub fn detri(&self) -> String { + self.date.format("M%m %d %Y").to_string() + } +} + +pub fn load(dir: &str) -> Result<Vec<Post>> { + let mut result: Vec<Post> = vec![]; + + for path in glob(&format!("{}/*.markdown", dir))?.filter_map(Result::ok) { + let body = fs::read_to_string(path.clone())?; + let (fm, content_offset) = frontmatter::Data::parse(body.clone().as_str())?; + let markup = &body[content_offset..]; + let date = NaiveDate::parse_from_str(&fm.clone().date, "%Y-%m-%d")?; + + result.push(Post { + front_matter: fm, + link: format!("{}/{}", dir, path.file_stem().unwrap().to_str().unwrap()), + body: markup.to_string(), + body_html: crate::app::markdown(&markup), + date: { + DateTime::<Utc>::from_utc( + NaiveDateTime::new(date, NaiveTime::from_hms(0, 0, 0)), + Utc, + ) + .with_timezone(&Utc) + .into() + }, + }) + } + + if result.len() == 0 { + Err(anyhow!("no posts loaded")) + } else { + result.sort(); + result.reverse(); + Ok(result) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use anyhow::Result; + + #[test] + fn blog() -> Result<()> { + load("blog")?; + Ok(()) + } + + #[test] + fn gallery() -> Result<()> { + load("gallery")?; + Ok(()) + } + + #[test] + fn talks() -> Result<()> { + load("talks")?; + Ok(()) + } +} |
