aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorXe Iaso <me@christine.website>2022-07-04 16:35:14 +0000
committerXe Iaso <me@christine.website>2022-07-04 16:35:14 +0000
commit7f6de2cb092cdd0675ae393a0a737a2c08329046 (patch)
tree29d74b18042f00f4b0d52cf3d9586135f5c9ce08
parent8b6056fc09320473577f458fa86bda26159ea43b (diff)
downloadxesite-7f6de2cb092cdd0675ae393a0a737a2c08329046.tar.xz
xesite-7f6de2cb092cdd0675ae393a0a737a2c08329046.zip
add _xesite_frontmatter extension
Signed-off-by: Xe Iaso <me@christine.website>
-rw-r--r--Cargo.lock10
-rw-r--r--Cargo.toml2
-rw-r--r--docs/jsonfeed_extensions.markdown27
-rw-r--r--lib/jsonfeed/Cargo.toml2
-rw-r--r--lib/jsonfeed/src/builder.rs8
-rw-r--r--lib/jsonfeed/src/item.rs9
-rw-r--r--lib/xesite_types/Cargo.toml10
-rw-r--r--lib/xesite_types/src/lib.rs37
-rw-r--r--src/post/frontmatter.rs162
-rw-r--r--src/post/mod.rs7
10 files changed, 179 insertions, 95 deletions
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,10 +3261,19 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
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<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,
})
}
}
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<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,
})
}
}
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<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,
+}
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<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))
}
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<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")