add _xesite_frontmatter extension

Signed-off-by: Xe Iaso <me@christine.website>
stanley
Cadey Ratio 5 months ago
parent 8b6056fc09
commit 7f6de2cb09
  1. 10
      Cargo.lock
  2. 2
      Cargo.toml
  3. 27
      docs/jsonfeed_extensions.markdown
  4. 2
      lib/jsonfeed/Cargo.toml
  5. 8
      lib/jsonfeed/src/builder.rs
  6. 9
      lib/jsonfeed/src/item.rs
  7. 10
      lib/xesite_types/Cargo.toml
  8. 37
      lib/xesite_types/src/lib.rs
  9. 162
      src/post/frontmatter.rs
  10. 7
      src/post/mod.rs

10
Cargo.lock generated

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

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

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

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

@ -90,6 +90,7 @@ pub struct ItemBuilder {
pub author: Option<Author>,
pub tags: Option<Vec<String>>,
pub attachments: Option<Vec<Attachment>>,
pub xesite_frontmater: Option<xesite_types::Frontmatter>,
}
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<Item> {
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,
})
}
}

@ -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<Author>,
pub tags: Option<Vec<String>>,
pub attachments: Option<Vec<Attachment>>,
// xesite extensions
pub xesite_frontmatter: Option<xesite_types::Frontmatter>,
}
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,
})
}
}

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

@ -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,
}

@ -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<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,
}
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,
};
}
'\n' | '\t' | ' ' => {
// ignore whitespace
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!("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,
}
_ => {
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,
};
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';
}
_ => 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();
// 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)?;
let fm: Data = serde_yaml::from_str(&payload)?;
Ok((fm, offset))
}
Ok((fm, offset))
}

@ -30,7 +30,7 @@ pub struct NewPost {
impl Into<xe_jsonfeed::Item> 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<xe_jsonfeed::Item> 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<String> = 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")

Loading…
Cancel
Save