add _xesite_frontmatter extension

Signed-off-by: Xe Iaso <me@christine.website>
This commit is contained in:
Cadey Ratio 2022-07-04 16:35:14 +00:00
parent 8b6056fc09
commit 7f6de2cb09
10 changed files with 182 additions and 98 deletions

10
Cargo.lock generated
View File

@ -3205,6 +3205,7 @@ dependencies = [
"serde", "serde",
"serde_derive", "serde_derive",
"serde_json", "serde_json",
"xesite_types",
] ]
[[package]] [[package]]
@ -3260,9 +3261,18 @@ dependencies = [
"url", "url",
"uuid 0.8.2", "uuid 0.8.2",
"xe_jsonfeed", "xe_jsonfeed",
"xesite_types",
"xml-rs", "xml-rs",
] ]
[[package]]
name = "xesite_types"
version = "0.1.0"
dependencies = [
"chrono",
"serde",
]
[[package]] [[package]]
name = "xml-rs" name = "xml-rs"
version = "0.8.4" version = "0.8.4"

View File

@ -48,6 +48,8 @@ xml-rs = "0.8"
url = "2" url = "2"
uuid = { version = "0.8", features = ["serde", "v4"] } uuid = { version = "0.8", features = ["serde", "v4"] }
xesite_types = { path = "./lib/xesite_types" }
# workspace dependencies # workspace dependencies
cfcache = { path = "./lib/cfcache" } cfcache = { path = "./lib/cfcache" }
xe_jsonfeed = { path = "./lib/jsonfeed" } xe_jsonfeed = { path = "./lib/jsonfeed" }

View File

@ -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.

View File

@ -13,3 +13,5 @@ error-chain = "0.12"
serde = "1" serde = "1"
serde_derive = "1" serde_derive = "1"
serde_json = "1" serde_json = "1"
xesite_types = { path = "../xesite_types" }

View File

@ -90,6 +90,7 @@ pub struct ItemBuilder {
pub author: Option<Author>, pub author: Option<Author>,
pub tags: Option<Vec<String>>, pub tags: Option<Vec<String>>,
pub attachments: Option<Vec<Attachment>>, pub attachments: Option<Vec<Attachment>>,
pub xesite_frontmater: Option<xesite_types::Frontmatter>,
} }
impl ItemBuilder { impl ItemBuilder {
@ -108,6 +109,7 @@ impl ItemBuilder {
author: None, author: None,
tags: None, tags: None,
attachments: None, attachments: None,
xesite_frontmater: None,
} }
} }
@ -180,6 +182,11 @@ impl ItemBuilder {
self self
} }
pub fn xesite_frontmatter(mut self, fm: xesite_types::Frontmatter) -> ItemBuilder {
self.xesite_frontmater = Some(fm);
self
}
pub fn build(self) -> Result<Item> { pub fn build(self) -> Result<Item> {
if self.id.is_none() || self.content.is_none() { if self.id.is_none() || self.content.is_none() {
return Err("missing field 'id' or 'content_*'".into()); return Err("missing field 'id' or 'content_*'".into());
@ -198,6 +205,7 @@ impl ItemBuilder {
author: self.author, author: self.author,
tags: self.tags, tags: self.tags,
attachments: self.attachments, attachments: self.attachments,
xesite_frontmatter: self.xesite_frontmater,
}) })
} }
} }

View File

@ -1,3 +1,4 @@
use std::collections::HashMap;
use std::default::Default; use std::default::Default;
use std::fmt; use std::fmt;
@ -31,6 +32,9 @@ pub struct Item {
pub author: Option<Author>, pub author: Option<Author>,
pub tags: Option<Vec<String>>, pub tags: Option<Vec<String>>,
pub attachments: Option<Vec<Attachment>>, pub attachments: Option<Vec<Attachment>>,
// xesite extensions
pub xesite_frontmatter: Option<xesite_types::Frontmatter>,
} }
impl Item { impl Item {
@ -55,6 +59,7 @@ impl Default for Item {
author: None, author: None,
tags: None, tags: None,
attachments: None, attachments: None,
xesite_frontmatter: None,
} }
} }
} }
@ -113,6 +118,9 @@ impl Serialize for Item {
if self.attachments.is_some() { if self.attachments.is_some() {
state.serialize_field("attachments", &self.attachments)?; state.serialize_field("attachments", &self.attachments)?;
} }
if self.xesite_frontmatter.is_some() {
state.serialize_field("_xesite_frontmatter", &self.xesite_frontmatter)?;
}
state.end() state.end()
} }
} }
@ -319,6 +327,7 @@ impl<'de> Deserialize<'de> for Item {
author, author,
tags, tags,
attachments, attachments,
xesite_frontmatter: None,
}) })
} }
} }

View File

@ -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" ] }

View File

@ -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<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub series: Option<String>,
#[serde(skip_serializing)]
pub tags: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub slides_link: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub image: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thumb: Option<String>,
#[serde(skip_serializing)]
pub redirect_to: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub vod: Option<Vod>,
}
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,
}

View File

@ -1,26 +1,6 @@
/// This code was borrowed from @fasterthanlime. /// This code was borrowed from @fasterthanlime.
use color_eyre::eyre::Result; use color_eyre::eyre::Result;
use serde::{Deserialize, Serialize}; pub use xesite_types::Frontmatter as Data;
#[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>,
pub redirect_to: Option<String>,
pub vod: Option<Vod>,
}
#[derive(Eq, PartialEq, Deserialize, Default, Debug, Serialize, Clone)]
pub struct Vod {
pub twitch: String,
pub youtube: String,
}
enum State { enum State {
SearchForStart, SearchForStart,
@ -37,85 +17,83 @@ enum Error {
Yaml(#[from] serde_yaml::Error), Yaml(#[from] serde_yaml::Error),
} }
impl Data { pub fn parse(input: &str) -> Result<(Data, usize)> {
pub fn parse(input: &str) -> Result<(Data, usize)> { let mut state = State::SearchForStart;
let mut state = State::SearchForStart;
let mut payload = None; let mut payload = None;
let offset; let offset;
let mut chars = input.char_indices(); let mut chars = input.char_indices();
'parse: loop { 'parse: loop {
let (idx, ch) = match chars.next() { let (idx, ch) = match chars.next() {
Some(x) => x, Some(x) => x,
None => return Err(Error::EOF)?, None => return Err(Error::EOF)?,
}; };
match &mut state { match &mut state {
State::SearchForStart => match ch { State::SearchForStart => match ch {
'-' => { '-' => {
state = State::ReadingMarker { state = State::ReadingMarker {
count: 1, count: 1,
end: false, 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"); ch => {
} buf.push(ch);
}, *line_start = ch == '\n';
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';
}
},
}
} }
// 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))
} }

View File

@ -30,7 +30,7 @@ pub struct NewPost {
impl Into<xe_jsonfeed::Item> for Post { impl Into<xe_jsonfeed::Item> for Post {
fn into(self) -> xe_jsonfeed::Item { fn into(self) -> xe_jsonfeed::Item {
let mut result = xe_jsonfeed::Item::builder() let mut result = xe_jsonfeed::Item::builder()
.title(self.front_matter.title) .title(self.front_matter.title.clone())
.content_html(self.body_html) .content_html(self.body_html)
.id(format!("https://xeiaso.net/{}", self.link)) .id(format!("https://xeiaso.net/{}", self.link))
.url(format!("https://xeiaso.net/{}", self.link)) .url(format!("https://xeiaso.net/{}", self.link))
@ -40,7 +40,8 @@ impl Into<xe_jsonfeed::Item> for Post {
.name("Xe Iaso") .name("Xe Iaso")
.url("https://xeiaso.net") .url("https://xeiaso.net")
.avatar("https://xeiaso.net/static/img/avatar.png"), .avatar("https://xeiaso.net/static/img/avatar.png"),
); )
.xesite_frontmatter(self.front_matter.clone());
let mut tags: Vec<String> = vec![]; let mut tags: Vec<String> = vec![];
@ -96,7 +97,7 @@ async fn read_post(
let body = fs::read_to_string(fname.clone()) let body = fs::read_to_string(fname.clone())
.await .await
.wrap_err_with(|| format!("can't read {:?}", fname))?; .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))?; .wrap_err_with(|| format!("can't parse frontmatter of {:?}", fname))?;
let body = &body[content_offset..]; let body = &body[content_offset..];
let date = NaiveDate::parse_from_str(&front_matter.clone().date, "%Y-%m-%d") let date = NaiveDate::parse_from_str(&front_matter.clone().date, "%Y-%m-%d")