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