diff options
author | Vika <vika@fireburn.ru> | 2023-07-31 15:58:07 +0300 |
---|---|---|
committer | Vika <vika@fireburn.ru> | 2023-07-31 15:58:07 +0300 |
commit | b1d2ad1ac8c1986a6255ef31045ea051f056711e (patch) | |
tree | 1eae65701891f2368eea0f714b58b8b9c938f7c6 | |
parent | b745013301369fff3d6f2e10702bba3549509c18 (diff) | |
download | kittybox-b1d2ad1ac8c1986a6255ef31045ea051f056711e.tar.zst |
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.
-rw-r--r-- | Cargo.lock | 37 | ||||
-rw-r--r-- | Cargo.toml | 2 | ||||
-rw-r--r-- | templates-neo/Cargo.toml | 38 | ||||
-rw-r--r-- | templates-neo/src/lib.rs | 1 | ||||
-rw-r--r-- | templates-neo/src/mf2.rs | 340 |
5 files changed, 417 insertions, 1 deletions
diff --git a/Cargo.lock b/Cargo.lock index 39323f4..774d5f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1309,6 +1309,21 @@ dependencies = [ ] [[package]] +name = "html" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc3a7577238029a2704c53d9571a1e5c67f36ea9f487b03b405df8b64030cacf" +dependencies = [ + "html-sys", +] + +[[package]] +name = "html-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05eabf4b1d6f9cf57ee8ddaab1a3065ae8394c5cb08d80f9b49d9ebe9307b56d" + +[[package]] name = "html5ever" version = "0.22.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1671,6 +1686,28 @@ dependencies = [ ] [[package]] +name = "kittybox-html" +version = "0.2.0" +dependencies = [ + "axum", + "chrono", + "ellipse", + "faker_rand", + "html", + "http", + "include_dir", + "kittybox-indieauth", + "kittybox-util", + "libflate", + "microformats", + "rand 0.8.5", + "serde_json", + "thiserror", + "url", + "walkdir", +] + +[[package]] name = "kittybox-indieauth" version = "0.1.0" dependencies = [ diff --git a/Cargo.toml b/Cargo.toml index 71ebc66..17e377b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,7 +47,7 @@ path = "src/bin/kittybox-mf2.rs" required-features = ["cli"] [workspace] -members = [".", "./util", "./templates", "./indieauth"] +members = [".", "./util", "./templates", "./indieauth", "./templates-neo"] default-members = [".", "./util", "./templates", "./indieauth"] [dependencies.kittybox-util] version = "0.1.0" 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<KnownClass>, + 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 + }, + #[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<url::Url>, + name: String, + note: Option<String>, + photo: Image, + pronouns: Vec<String> +} + +impl TryFrom<Item> for Card { + type Error = Error; + + fn try_from(card: Item) -> Result<Self, Self::Error> { + 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<PropertyValue> for Card { + type Error = Error; + + 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 }) + } + } +} + +pub struct Cite { + uid: url::Url, + url: Vec<url::Url>, + in_reply_to: Option<Vec<Citation>>, + author: Card, + published: Option<chrono::DateTime<chrono::FixedOffset>>, + content: Content +} + +impl TryFrom<Item> for Cite { + type Error = Error; + + fn try_from(cite: Item) -> Result<Self, Self::Error> { + 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<PropertyValue> for Citation { + type Error = Error; + fn try_from(v: PropertyValue) -> Result<Self, Self::Error> { + 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<Content> 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<url::Url>, + in_reply_to: Option<Citation>, + author: Card, + category: Vec<String>, + syndication: Vec<url::Url>, + published: chrono::DateTime<chrono::FixedOffset>, + 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 + }) + } + + 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::<Result<Vec<_>, _>>()?, + 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::<Result<Vec<_>, _>>()?, + 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)) => { + 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 + }) + ) + }) + ) + } +} |