From 7f6de2cb092cdd0675ae393a0a737a2c08329046 Mon Sep 17 00:00:00 2001 From: Xe Iaso Date: Mon, 4 Jul 2022 16:35:14 +0000 Subject: [PATCH] add _xesite_frontmatter extension Signed-off-by: Xe Iaso --- Cargo.lock | 10 ++ Cargo.toml | 2 + docs/jsonfeed_extensions.markdown | 27 +++++ lib/jsonfeed/Cargo.toml | 2 + lib/jsonfeed/src/builder.rs | 8 ++ lib/jsonfeed/src/item.rs | 9 ++ lib/xesite_types/Cargo.toml | 10 ++ lib/xesite_types/src/lib.rs | 37 +++++++ src/post/frontmatter.rs | 168 +++++++++++++----------------- src/post/mod.rs | 7 +- 10 files changed, 182 insertions(+), 98 deletions(-) create mode 100644 docs/jsonfeed_extensions.markdown create mode 100644 lib/xesite_types/Cargo.toml create mode 100644 lib/xesite_types/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 591696a..b86dbe7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3205,6 +3205,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", + "xesite_types", ] [[package]] @@ -3260,9 +3261,18 @@ dependencies = [ "url", "uuid 0.8.2", "xe_jsonfeed", + "xesite_types", "xml-rs", ] +[[package]] +name = "xesite_types" +version = "0.1.0" +dependencies = [ + "chrono", + "serde", +] + [[package]] name = "xml-rs" version = "0.8.4" diff --git a/Cargo.toml b/Cargo.toml index 62f4a5a..319a27f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,8 @@ xml-rs = "0.8" url = "2" uuid = { version = "0.8", features = ["serde", "v4"] } +xesite_types = { path = "./lib/xesite_types" } + # workspace dependencies cfcache = { path = "./lib/cfcache" } xe_jsonfeed = { path = "./lib/jsonfeed" } diff --git a/docs/jsonfeed_extensions.markdown b/docs/jsonfeed_extensions.markdown new file mode 100644 index 0000000..63a8aa2 --- /dev/null +++ b/docs/jsonfeed_extensions.markdown @@ -0,0 +1,27 @@ +# JSON Feed Extensions + +Here is the documentation of all of my JSON Feed extensions. I have created +these JSON Feed extensions in order to give users more metadata about my +articles and talks. + +## `_xesite_frontmatter` + +This extension is added to [JSON Feed +Items](https://www.jsonfeed.org/version/1.1/#items-a-name-items-a) and gives +readers a copy of the frontmatter data that I annotate my posts with. The +contents of this will vary by post, but will have any of the following fields: + +* `about` (required, string) is a link to this documentation. It gives readers + of the JSON Feed information about what this extension does. This is for + informational purposes only and can safely be ignored by programs. +* `series` (optional, string) is the optional blogpost series name that this + item belongs to. When I post multiple posts about the same topic, I will + usually set the `series` to the same value so that it is more discoverable [on + my series index page](https://xeiaso.net/blog/series). +* `slides_link` (optional, string) is a link to the PDF containing the slides + for a given talk. This is always set on talks, but is technically optional + because not everything I do is a talk. +* `vod` (optional, string) is an object that describes where you can watch the + Video On Demand (vod) for the writing process of a post. This is an object + that always contains the fields `twitch` and `youtube`. These will be URLs to + the videos so that you can watch them on demand. diff --git a/lib/jsonfeed/Cargo.toml b/lib/jsonfeed/Cargo.toml index 99172fd..ad17ccd 100644 --- a/lib/jsonfeed/Cargo.toml +++ b/lib/jsonfeed/Cargo.toml @@ -13,3 +13,5 @@ error-chain = "0.12" serde = "1" serde_derive = "1" serde_json = "1" + +xesite_types = { path = "../xesite_types" } diff --git a/lib/jsonfeed/src/builder.rs b/lib/jsonfeed/src/builder.rs index 640a280..4ce47a4 100644 --- a/lib/jsonfeed/src/builder.rs +++ b/lib/jsonfeed/src/builder.rs @@ -90,6 +90,7 @@ pub struct ItemBuilder { pub author: Option, pub tags: Option>, pub attachments: Option>, + pub xesite_frontmater: Option, } impl ItemBuilder { @@ -108,6 +109,7 @@ impl ItemBuilder { author: None, tags: None, attachments: None, + xesite_frontmater: None, } } @@ -180,6 +182,11 @@ impl ItemBuilder { self } + pub fn xesite_frontmatter(mut self, fm: xesite_types::Frontmatter) -> ItemBuilder { + self.xesite_frontmater = Some(fm); + self + } + pub fn build(self) -> Result { if self.id.is_none() || self.content.is_none() { return Err("missing field 'id' or 'content_*'".into()); @@ -198,6 +205,7 @@ impl ItemBuilder { author: self.author, tags: self.tags, attachments: self.attachments, + xesite_frontmatter: self.xesite_frontmater, }) } } diff --git a/lib/jsonfeed/src/item.rs b/lib/jsonfeed/src/item.rs index 0f7d6ab..7b5d734 100644 --- a/lib/jsonfeed/src/item.rs +++ b/lib/jsonfeed/src/item.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::default::Default; use std::fmt; @@ -31,6 +32,9 @@ pub struct Item { pub author: Option, pub tags: Option>, pub attachments: Option>, + + // xesite extensions + pub xesite_frontmatter: Option, } impl Item { @@ -55,6 +59,7 @@ impl Default for Item { author: None, tags: None, attachments: None, + xesite_frontmatter: None, } } } @@ -113,6 +118,9 @@ impl Serialize for Item { if self.attachments.is_some() { state.serialize_field("attachments", &self.attachments)?; } + if self.xesite_frontmatter.is_some() { + state.serialize_field("_xesite_frontmatter", &self.xesite_frontmatter)?; + } state.end() } } @@ -319,6 +327,7 @@ impl<'de> Deserialize<'de> for Item { author, tags, attachments, + xesite_frontmatter: None, }) } } diff --git a/lib/xesite_types/Cargo.toml b/lib/xesite_types/Cargo.toml new file mode 100644 index 0000000..5aec478 --- /dev/null +++ b/lib/xesite_types/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "xesite_types" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +chrono = { version = "0.4", features = [ "serde" ] } +serde = { version = "1.0", features = [ "derive" ] } diff --git a/lib/xesite_types/src/lib.rs b/lib/xesite_types/src/lib.rs new file mode 100644 index 0000000..68ae4c4 --- /dev/null +++ b/lib/xesite_types/src/lib.rs @@ -0,0 +1,37 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Eq, PartialEq, Deserialize, Default, Debug, Serialize, Clone)] +pub struct Frontmatter { + #[serde(default = "frontmatter_about")] + pub about: String, + #[serde(skip_serializing)] + pub title: String, + #[serde(skip_serializing)] + pub date: String, + #[serde(skip_serializing)] + pub author: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub series: Option, + #[serde(skip_serializing)] + pub tags: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub slides_link: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub image: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub thumb: Option, + #[serde(skip_serializing)] + pub redirect_to: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub vod: Option, +} + +fn frontmatter_about() -> String { + "https://xeiaso.net/blog/api-jsonfeed-extensions#_xesite_frontmatter".to_string() +} + +#[derive(Eq, PartialEq, Deserialize, Default, Debug, Serialize, Clone)] +pub struct Vod { + pub twitch: String, + pub youtube: String, +} diff --git a/src/post/frontmatter.rs b/src/post/frontmatter.rs index fa4c65c..67b96ab 100644 --- a/src/post/frontmatter.rs +++ b/src/post/frontmatter.rs @@ -1,26 +1,6 @@ /// This code was borrowed from @fasterthanlime. use color_eyre::eyre::Result; -use serde::{Deserialize, Serialize}; - -#[derive(Eq, PartialEq, Deserialize, Default, Debug, Serialize, Clone)] -pub struct Data { - pub title: String, - pub date: String, - pub series: Option, - pub tags: Option>, - pub slides_link: Option, - pub image: Option, - pub thumb: Option, - pub show: Option, - pub redirect_to: Option, - pub vod: Option, -} - -#[derive(Eq, PartialEq, Deserialize, Default, Debug, Serialize, Clone)] -pub struct Vod { - pub twitch: String, - pub youtube: String, -} +pub use xesite_types::Frontmatter as Data; enum State { SearchForStart, @@ -37,85 +17,83 @@ enum Error { Yaml(#[from] serde_yaml::Error), } -impl Data { - pub fn parse(input: &str) -> Result<(Data, usize)> { - let mut state = State::SearchForStart; +pub fn parse(input: &str) -> Result<(Data, usize)> { + let mut state = State::SearchForStart; - let mut payload = None; - let offset; + 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, + 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, }; } - '\n' | '\t' | ' ' => { - // ignore whitespace + } + _ => panic!("Expected newline, got {:?}", ch), + }, + 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!(); } - _ => { - 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 {:?}", ch), - }, - 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'; - } - }, - } + } + 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)) } + + // unwrap justification: option set in state machine, Rust can't statically analyze it + let payload = payload.unwrap(); + + let fm: Data = serde_yaml::from_str(&payload)?; + + Ok((fm, offset)) } diff --git a/src/post/mod.rs b/src/post/mod.rs index f24b29c..5c2ed2d 100644 --- a/src/post/mod.rs +++ b/src/post/mod.rs @@ -30,7 +30,7 @@ pub struct NewPost { impl Into for Post { fn into(self) -> xe_jsonfeed::Item { let mut result = xe_jsonfeed::Item::builder() - .title(self.front_matter.title) + .title(self.front_matter.title.clone()) .content_html(self.body_html) .id(format!("https://xeiaso.net/{}", self.link)) .url(format!("https://xeiaso.net/{}", self.link)) @@ -40,7 +40,8 @@ impl Into for Post { .name("Xe Iaso") .url("https://xeiaso.net") .avatar("https://xeiaso.net/static/img/avatar.png"), - ); + ) + .xesite_frontmatter(self.front_matter.clone()); let mut tags: Vec = vec![]; @@ -96,7 +97,7 @@ async fn read_post( let body = fs::read_to_string(fname.clone()) .await .wrap_err_with(|| format!("can't read {:?}", fname))?; - let (front_matter, content_offset) = frontmatter::Data::parse(body.clone().as_str()) + let (front_matter, content_offset) = frontmatter::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")