From b1d2ad1ac8c1986a6255ef31045ea051f056711e Mon Sep 17 00:00:00 2001 From: Vika Date: Mon, 31 Jul 2023 15:58:07 +0300 Subject: templates-neo: init This is an experimental approach to templates, using `yoshuawuyts`'s `html` crate. The crate itself has a promising API, but is notably incomplete. --- templates-neo/Cargo.toml | 38 ++++++ templates-neo/src/lib.rs | 1 + templates-neo/src/mf2.rs | 340 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 379 insertions(+) create mode 100644 templates-neo/Cargo.toml create mode 100644 templates-neo/src/lib.rs create mode 100644 templates-neo/src/mf2.rs (limited to 'templates-neo') diff --git a/templates-neo/Cargo.toml b/templates-neo/Cargo.toml new file mode 100644 index 0000000..c0b426c --- /dev/null +++ b/templates-neo/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "kittybox-html" +version = "0.2.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[build-dependencies] +libflate = "^2.0.0" +walkdir = "^2.3.2" + +[dev-dependencies] +faker_rand = "^0.1.1" +rand = "^0.8.5" + +[dependencies] +ellipse = "^0.2.0" +http = "^0.2.7" +html = "^0.5.2" +serde_json = "^1.0.64" +include_dir = "^0.7.2" +axum = "^0.6.18" +thiserror = "1.0.43" +[dependencies.url] +# URL library for Rust, based on the WHATWG URL Standard +version = "^2.2.1" +features = ["serde"] +[dependencies.chrono] +version = "^0.4.19" +features = ["serde"] +[dependencies.kittybox-util] +version = "0.1.0" +path = "../util" +[dependencies.kittybox-indieauth] +version = "0.1.0" +path = "../indieauth" +[dependencies.microformats] +version="^0.3.0" \ No newline at end of file diff --git a/templates-neo/src/lib.rs b/templates-neo/src/lib.rs new file mode 100644 index 0000000..c5bd925 --- /dev/null +++ b/templates-neo/src/lib.rs @@ -0,0 +1 @@ +pub mod mf2; diff --git a/templates-neo/src/mf2.rs b/templates-neo/src/mf2.rs new file mode 100644 index 0000000..706a301 --- /dev/null +++ b/templates-neo/src/mf2.rs @@ -0,0 +1,340 @@ +use std::{collections::HashMap, borrow::Cow}; + +use html::{media::builders, inline_text::Anchor, content::builders::ArticleBuilder}; +use microformats::types::{Class, KnownClass, Item, PropertyValue, temporal::Value as Temporal, Fragment}; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("wrong mf2 class, expected {expected:?}, got {got:?}")] + WrongClass { + expected: Vec, + got: Vec + }, + #[error("entry lacks `uid` property")] + NoUid, + #[error("unexpected type of property value: expected {expected}, got {got:?}")] + WrongValueType { + expected: &'static str, + got: PropertyValue + }, + #[error("missing property: {0}")] + MissingProperty(&'static str) +} + +pub enum Image { + Plain(url::Url), + Accessible { + src: url::Url, + alt: String + } +} + +impl Image { + 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) + } + } +} + +pub struct Card { + uid: url::Url, + urls: Vec, + name: String, + note: Option, + photo: Image, + pronouns: Vec +} + +impl TryFrom for Card { + type Error = Error; + + fn try_from(card: Item) -> Result { + if card.r#type.as_slice() != [Class::Known(KnownClass::Card)] { + return Err(Error::WrongClass { + expected: vec![KnownClass::Card], + got: card.r#type + }) + } + + todo!() + } +} + +impl Card { + 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) + ) + } + + 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) + ) + }); + + if !self.pronouns.is_empty() { + article.span(move |span| { + span.text("("); + self.pronouns.into_iter().for_each(|p| { span.text(p); }); + span.text(")") + }); + } + + if let Some(note) = self.note { + article.paragraph(move |p| p.class("p-note").text(note)); + } + + if !urls.is_empty() { + article.paragraph(|p| p.text("Can be found elsewhere at:")); + 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() + // XXX https://github.com/yoshuawuyts/html/issues/51 + .to_string() + })); + } + + ul + }); + } + + article + } +} + +impl TryFrom for Card { + type Error = Error; + + fn try_from(v: PropertyValue) -> Result { + match v { + PropertyValue::Item(item) => item.take().try_into(), + other => Err(Error::WrongValueType { expected: "h-card", got: other }) + } + } +} + +pub struct Cite { + uid: url::Url, + url: Vec, + in_reply_to: Option>, + author: Card, + published: Option>, + content: Content +} + +impl TryFrom for Cite { + type Error = Error; + + fn try_from(cite: Item) -> Result { + if cite.r#type.as_slice() != [Class::Known(KnownClass::Cite)] { + return Err(Error::WrongClass { + expected: vec![KnownClass::Cite], + got: cite.r#type + }) + } + + todo!() + } + +} + +pub enum Citation { + Brief(url::Url), + Full(Cite) +} + +impl TryFrom for Citation { + type Error = Error; + fn try_from(v: PropertyValue) -> Result { + match v { + PropertyValue::Url(url) => Ok(Self::Brief(url)), + PropertyValue::Item(item) => Ok(Self::Full(item.take().try_into()?)), + other => Err(Error::WrongValueType { + expected: "url or h-cite", + got: other + }) + } + } +} + +pub struct Content(Fragment); + +impl From for html::content::Main { + fn from(content: Content) -> Self { + let mut builder = Self::builder(); + builder + .class("e-content") + .text(content.0.html); + if let Some(lang) = content.0.lang { + builder.lang(Cow::Owned(lang)); + } + builder.build() + } +} + +pub struct Entry { + uid: url::Url, + url: Vec, + in_reply_to: Option, + author: Card, + category: Vec, + syndication: Vec, + published: chrono::DateTime, + content: Content +} + + +impl TryFrom for Entry { + type Error = Error; + fn try_from(entry: Item) -> Result { + if entry.r#type.as_slice() != [Class::Known(KnownClass::Entry)] { + return Err(Error::WrongClass { + expected: vec![KnownClass::Entry], + got: entry.r#type + }) + } + + let mut props = entry.properties.take(); + let uid = { + let uids = props.remove("uid").ok_or(Error::NoUid)?; + if let Some(PropertyValue::Url(uid)) = uids.into_iter().take(1).next() { + uid + } else { + 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") + .unwrap_or_default() + .into_iter() + .next() + .map(|v| v.try_into()) + .transpose()?, + 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") + .unwrap_or_default() + .into_iter() + .map(|v| match v { + PropertyValue::Plain(string) => Ok(string), + other => Err(Error::WrongValueType { + expected: "string", + got: other + }) + }) + .collect::, _>>()?, + 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 + }) + }) + .collect::, _>>()?, + published: props.remove("published") + .unwrap_or_default() + .into_iter() + .next() + .map(|v| -> Result, Error> { + match v { + PropertyValue::Temporal(Temporal::Timestamp(dt)) => { + todo!() + }, + other => Err(Error::WrongValueType { + expected: "timestamp", + got: other + }) + } + }) + .ok_or(Error::MissingProperty("published"))??, + content: props.remove("content") + .unwrap_or_default() + .into_iter() + .next() + .ok_or(Error::MissingProperty("content")) + .and_then(|v| match v { + PropertyValue::Fragment(fragment) => Ok(Content(fragment)), + other => Err(Error::WrongValueType { + expected: "html", + got: other + }) + })?, + }) + } +} + +impl Entry { + 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.to_string()) + .text(self.published.to_rfc3339_opts( + chrono::SecondsFormat::Secs, false + )) + .build() + // XXX https://github.com/yoshuawuyts/html/issues/51 + .to_string() + ))) + .division(|div| div + .text("Tagged") + .unordered_list(|ul| { + for category in self.category { + ul.list_item(|li| li + .class("p-category") + .text(category) + ); + } + + ul + }) + ) + }) + ) + } +} -- cgit 1.4.1