add _xesite_frontmatter extension
Signed-off-by: Xe Iaso <me@christine.website>
This commit is contained in:
parent
8b6056fc09
commit
7f6de2cb09
|
@ -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,
|
||||
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))
|
||||
}
|
||||
|
|
|
@ -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…
Reference in New Issue