aboutsummaryrefslogtreecommitdiff
path: root/src/post
diff options
context:
space:
mode:
authorChristine Dodrill <me@christine.website>2020-07-16 15:32:30 -0400
committerGitHub <noreply@github.com>2020-07-16 15:32:30 -0400
commit385d25c9f96c0acd5d932488e3bd0ed36ceb4dd7 (patch)
treeaf789f7250519b23038a7e5ea0ae7f4f4c1ffdfc /src/post
parent449e934246c82d90dd0aac2644d67f928befeeb4 (diff)
downloadxesite-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.rs114
-rw-r--r--src/post/mod.rs178
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(())
+ }
+}