diff options
-rw-r--r-- | templates-neo/src/main.rs | 7 | ||||
-rw-r--r-- | templates-neo/src/mf2.rs | 315 |
2 files changed, 184 insertions, 138 deletions
diff --git a/templates-neo/src/main.rs b/templates-neo/src/main.rs index 54afbb8..d2c7ea9 100644 --- a/templates-neo/src/main.rs +++ b/templates-neo/src/main.rs @@ -2,7 +2,6 @@ use std::io::Write; use kittybox_html::mf2::Entry; - fn main() { let mf2 = serde_json::from_reader::<_, microformats::types::Item>(std::io::stdin()).unwrap(); let entry = Entry::try_from(mf2).unwrap(); @@ -11,6 +10,8 @@ fn main() { entry.build(&mut article); let mut stdout = std::io::stdout().lock(); - stdout.write_all(article.build().to_string().as_bytes()).unwrap(); + stdout + .write_all(article.build().to_string().as_bytes()) + .unwrap(); stdout.write_all(b"\n").unwrap(); -} \ No newline at end of file +} diff --git a/templates-neo/src/mf2.rs b/templates-neo/src/mf2.rs index 194e02b..8190720 100644 --- a/templates-neo/src/mf2.rs +++ b/templates-neo/src/mf2.rs @@ -1,21 +1,27 @@ -use std::{collections::HashMap, borrow::Cow}; +use std::{borrow::Cow, collections::HashMap}; -use html::{media::builders, inline_text::Anchor, content::builders::ArticleBuilder}; -use microformats::types::{Class, KnownClass, Item, PropertyValue, temporal::Value as Temporal, Fragment}; +use html::{ + content::builders::{ArticleBuilder, SectionBuilder}, + inline_text::Anchor, + media::builders, +}; +use microformats::types::{ + temporal::Value as Temporal, Class, Fragment, Item, KnownClass, PropertyValue, +}; #[derive(Debug, thiserror::Error)] pub enum Error { #[error("wrong mf2 class, expected {expected:?}, got {got:?}")] WrongClass { expected: Vec<KnownClass>, - got: Vec<Class> + got: Vec<Class>, }, #[error("entry lacks `uid` property")] NoUid, #[error("unexpected type of property value: expected {expected}, got {got:?}")] WrongValueType { expected: &'static str, - got: PropertyValue + got: PropertyValue, }, #[error("missing property: {0}")] MissingProperty(&'static str), @@ -23,17 +29,17 @@ pub enum Error { pub enum Image { Plain(url::Url), - Accessible { - src: url::Url, - alt: String - } + Accessible { src: url::Url, alt: String }, } impl Image { - pub fn build(self, img: &mut html::media::builders::ImageBuilder) -> &mut html::media::builders::ImageBuilder { + pub fn build( + self, + img: &mut html::media::builders::ImageBuilder, + ) -> &mut html::media::builders::ImageBuilder { match self { Image::Plain(url) => img.src(String::from(url)), - Image::Accessible { src, alt } => img.src(String::from(src)).alt(alt) + Image::Accessible { src, alt } => img.src(String::from(src)).alt(alt), } } } @@ -44,7 +50,7 @@ pub struct Card { name: String, note: Option<String>, photo: Image, - pronouns: Vec<String> + pronouns: Vec<String>, } impl TryFrom<Item> for Card { @@ -54,8 +60,8 @@ impl TryFrom<Item> for Card { if card.r#type.as_slice() != [Class::Known(KnownClass::Card)] { return Err(Error::WrongClass { expected: vec![KnownClass::Card], - got: card.r#type - }) + got: card.r#type, + }); } let mut props = card.properties.take(); @@ -64,19 +70,26 @@ impl TryFrom<Item> for Card { if let Some(PropertyValue::Url(uid)) = uids.into_iter().take(1).next() { uid } else { - return Err(Error::NoUid) + return Err(Error::NoUid); } }; Ok(Self { uid, - urls: props.remove("url").unwrap_or_default().into_iter() - .filter_map(|v| if let PropertyValue::Url(url) = v { - Some(url) - } else { - None - }).collect(), - name: props.remove("name") + urls: props + .remove("url") + .unwrap_or_default() + .into_iter() + .filter_map(|v| { + if let PropertyValue::Url(url) = v { + Some(url) + } else { + None + } + }) + .collect(), + name: props + .remove("name") .unwrap_or_default() .into_iter() .next() @@ -85,10 +98,11 @@ impl TryFrom<Item> for Card { PropertyValue::Plain(plain) => Ok(plain), other => Err(Error::WrongValueType { expected: "string", - got: other - }) + got: other, + }), })?, - note: props.remove("note") + note: props + .remove("note") .unwrap_or_default() .into_iter() .next() @@ -96,11 +110,12 @@ impl TryFrom<Item> for Card { PropertyValue::Plain(plain) => Some(Ok(plain)), other => Some(Err(Error::WrongValueType { expected: "string", - got: other - })) + got: other, + })), }) .transpose()?, - photo: props.remove("photo") + photo: props + .remove("photo") .unwrap_or_default() .into_iter() .next() @@ -109,58 +124,66 @@ impl TryFrom<Item> for Card { PropertyValue::Url(url) => Ok(Image::Plain(url)), PropertyValue::Image(image) => Ok(Image::Accessible { src: image.src, - alt: image.alt + alt: image.alt, }), other => Err(Error::WrongValueType { expected: "string", - got: other - }) + got: other, + }), })?, - pronouns: props.remove("pronoun") + pronouns: props + .remove("pronoun") .unwrap_or_default() .into_iter() .map(|v| match v { PropertyValue::Plain(plain) => Ok(plain), other => Err(Error::WrongValueType { expected: "string", - got: other - }) + got: other, + }), }) - .collect::<Result<Vec<String>, _>>()? + .collect::<Result<Vec<String>, _>>()?, }) } } impl Card { - pub fn build_section(self, section: &mut html::content::builders::SectionBuilder) -> &mut html::content::builders::SectionBuilder { - section - .class("mini-h-card") - .anchor(|a| a - .class("larger u-author") + pub fn build_section( + self, + section: &mut html::content::builders::SectionBuilder, + ) -> &mut html::content::builders::SectionBuilder { + section.class("mini-h-card").anchor(|a| { + a.class("larger u-author") .href(String::from(self.uid)) .image(move |img| self.photo.build(img).loading("lazy")) .text(self.name) - ) + }) } - pub fn build(self, article: &mut html::content::builders::ArticleBuilder) -> &mut html::content::builders::ArticleBuilder { + pub fn build( + self, + article: &mut html::content::builders::ArticleBuilder, + ) -> &mut html::content::builders::ArticleBuilder { let urls: Vec<_> = self.urls.into_iter().filter(|u| *u != self.uid).collect(); article .class("h-card") .image(move |builder| self.photo.build(builder)) .heading_1(move |builder| { - builder.anchor(|builder| builder - .class("u-url u-uid p-name") - .href(String::from(self.uid)) - .text(self.name) - ) + builder.anchor(|builder| { + builder + .class("u-url u-uid p-name") + .href(String::from(self.uid)) + .text(self.name) + }) }); if !self.pronouns.is_empty() { article.span(move |span| { span.text("("); - self.pronouns.into_iter().for_each(|p| { span.text(p); }); + self.pronouns.into_iter().for_each(|p| { + span.text(p); + }); span.text(")") }); } @@ -174,12 +197,9 @@ impl Card { article.unordered_list(move |ul| { for url in urls { let url = String::from(url); - ul.list_item(move |li| li.push({ - Anchor::builder() - .href(url.clone()) - .text(url) - .build() - })); + ul.list_item(move |li| { + li.push({ Anchor::builder().href(url.clone()).text(url).build() }) + }); } ul @@ -196,7 +216,10 @@ impl TryFrom<PropertyValue> for Card { fn try_from(v: PropertyValue) -> Result<Self, Self::Error> { match v { PropertyValue::Item(item) => item.take().try_into(), - other => Err(Error::WrongValueType { expected: "h-card", got: other }) + other => Err(Error::WrongValueType { + expected: "h-card", + got: other, + }), } } } @@ -207,7 +230,7 @@ pub struct Cite { in_reply_to: Option<Vec<Citation>>, author: Card, published: Option<chrono::DateTime<chrono::FixedOffset>>, - content: Content + content: Content, } impl TryFrom<Item> for Cite { @@ -217,18 +240,17 @@ impl TryFrom<Item> for Cite { if cite.r#type.as_slice() != [Class::Known(KnownClass::Cite)] { return Err(Error::WrongClass { expected: vec![KnownClass::Cite], - got: cite.r#type - }) + got: cite.r#type, + }); } todo!() } - } pub enum Citation { Brief(url::Url), - Full(Cite) + Full(Cite), } impl TryFrom<PropertyValue> for Citation { @@ -239,8 +261,8 @@ impl TryFrom<PropertyValue> for Citation { PropertyValue::Item(item) => Ok(Self::Full(item.take().try_into()?)), other => Err(Error::WrongValueType { expected: "url or h-cite", - got: other - }) + got: other, + }), } } } @@ -250,9 +272,7 @@ pub struct Content(Fragment); impl From<Content> for html::content::Main { fn from(content: Content) -> Self { let mut builder = Self::builder(); - builder - .class("e-content") - .text(content.0.html); + builder.class("e-content").text(content.0.html); if let Some(lang) = content.0.lang { builder.lang(Cow::Owned(lang)); } @@ -268,18 +288,17 @@ pub struct Entry { category: Vec<String>, syndication: Vec<url::Url>, published: chrono::DateTime<chrono::FixedOffset>, - content: Content + content: Content, } - impl TryFrom<Item> for Entry { type Error = Error; fn try_from(entry: Item) -> Result<Self, Self::Error> { if entry.r#type.as_slice() != [Class::Known(KnownClass::Entry)] { return Err(Error::WrongClass { expected: vec![KnownClass::Entry], - got: entry.r#type - }) + got: entry.r#type, + }); } let mut props = entry.properties.take(); @@ -288,75 +307,96 @@ impl TryFrom<Item> for Entry { if let Some(PropertyValue::Url(uid)) = uids.into_iter().take(1).next() { uid } else { - return Err(Error::NoUid) + return Err(Error::NoUid); } }; Ok(Entry { uid, - url: props.remove("url").unwrap_or_default().into_iter() - .filter_map(|v| if let PropertyValue::Url(url) = v { - Some(url) - } else { - None - }).collect(), - in_reply_to: props.remove("in-reply-to") + url: props + .remove("url") + .unwrap_or_default() + .into_iter() + .filter_map(|v| { + if let PropertyValue::Url(url) = v { + Some(url) + } else { + None + } + }) + .collect(), + in_reply_to: props + .remove("in-reply-to") .unwrap_or_default() .into_iter() .next() .map(|v| v.try_into()) .transpose()?, - author: props.remove("author") + author: props + .remove("author") .unwrap_or_default() .into_iter() .next() .map(|v| v.try_into()) .transpose()? .ok_or(Error::MissingProperty("author"))?, - category: props.remove("category") + category: props + .remove("category") .unwrap_or_default() .into_iter() .map(|v| match v { PropertyValue::Plain(string) => Ok(string), other => Err(Error::WrongValueType { expected: "string", - got: other - }) + got: other, + }), }) .collect::<Result<Vec<_>, _>>()?, - syndication: props.remove("syndication") + syndication: props + .remove("syndication") .unwrap_or_default() .into_iter() .map(|v| match v { PropertyValue::Url(url) => Ok(url), other => Err(Error::WrongValueType { expected: "link", - got: other - }) + got: other, + }), }) .collect::<Result<Vec<_>, _>>()?, - published: props.remove("published") + published: props + .remove("published") .unwrap_or_default() .into_iter() .next() - .map(|v| -> Result<chrono::DateTime<chrono::FixedOffset>, Error> { - match v { - PropertyValue::Temporal(Temporal::Timestamp(dt)) => { - // This is incredibly sketchy. - let (date, time, offset) = (dt.date.clone().unwrap().data, dt.as_time().unwrap().data.clone(), dt.as_time().unwrap().offset.unwrap().data); - - date.and_time(time).and_local_timezone(offset).single().ok_or_else(|| Error::WrongValueType { - expected: "datetime with timezone", - got: PropertyValue::Temporal(Temporal::Timestamp(dt)) - }) - }, - other => Err(Error::WrongValueType { - expected: "timestamp", - got: other - }) - } - }) + .map( + |v| -> Result<chrono::DateTime<chrono::FixedOffset>, Error> { + match v { + PropertyValue::Temporal(Temporal::Timestamp(dt)) => { + // This is incredibly sketchy. + let (date, time, offset) = ( + dt.date.clone().unwrap().data, + dt.as_time().unwrap().data.clone(), + dt.as_time().unwrap().offset.unwrap().data, + ); + + date.and_time(time) + .and_local_timezone(offset) + .single() + .ok_or_else(|| Error::WrongValueType { + expected: "datetime with timezone", + got: PropertyValue::Temporal(Temporal::Timestamp(dt)), + }) + } + other => Err(Error::WrongValueType { + expected: "timestamp", + got: other, + }), + } + }, + ) .ok_or(Error::MissingProperty("published"))??, - content: props.remove("content") + content: props + .remove("content") .unwrap_or_default() .into_iter() .next() @@ -365,8 +405,8 @@ impl TryFrom<Item> for Entry { PropertyValue::Fragment(fragment) => Ok(Content(fragment)), other => Err(Error::WrongValueType { expected: "html", - got: other - }) + got: other, + }), })?, }) } @@ -376,44 +416,49 @@ impl Entry { pub fn build(self, article: &mut ArticleBuilder) -> &mut ArticleBuilder { article .class("h-entry") - .header(|header| header - .class("metadata") - .section(|section| self.author.build_section(section)) - .section(|div| { - div - .division(|div| div - .anchor(|a| a - .class("u-url u-uid") - .href(String::from(self.uid)) - .push(html::inline_text::Time::builder() - .text(self.published.format("%Y-%m-%d %a %H:%M:%S %z").to_string()) - .date_time(self.published.to_rfc3339_opts( - chrono::SecondsFormat::Secs, false - )) - .build() - ))) - .division(|div| div - .text("Tagged") - .unordered_list(|ul| { - for category in self.category { - ul.list_item(|li| li - .class("p-category") - .text(category) - ); - } - - ul + .header(|header| { + header + .class("metadata") + .section(|section| self.author.build_section(section)) + .section(|section| { + section + .division(|div| { + div.anchor(|a| { + a.class("u-url u-uid").href(String::from(self.uid)).push( + html::inline_text::Time::builder() + .text( + self.published + .format("%Y-%m-%d %a %H:%M:%S %z") + .to_string(), + ) + .date_time(self.published.to_rfc3339_opts( + chrono::SecondsFormat::Secs, + false, + )) + .build(), + ) + }) + }) + .division(|div| { + div.text("Tagged").unordered_list(|ul| { + for category in self.category { + ul.list_item(|li| li.class("p-category").text(category)); + } + + ul + }) }) - ) }) - ) + }) .main(|main| { if let Some(lang) = self.content.0.lang { main.lang(lang); } + // XXX .text() and .push() are completely equivalent + // since .text() does no escaping main.push(self.content.0.html) }) - + .footer(|footer| footer) } } |