about summary refs log tree commit diff
diff options
context:
space:
mode:
authorVika <vika@fireburn.ru>2023-07-31 15:58:07 +0300
committerVika <vika@fireburn.ru>2023-07-31 15:58:07 +0300
commitb1d2ad1ac8c1986a6255ef31045ea051f056711e (patch)
tree1eae65701891f2368eea0f714b58b8b9c938f7c6
parentb745013301369fff3d6f2e10702bba3549509c18 (diff)
downloadkittybox-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.lock37
-rw-r--r--Cargo.toml2
-rw-r--r--templates-neo/Cargo.toml38
-rw-r--r--templates-neo/src/lib.rs1
-rw-r--r--templates-neo/src/mf2.rs340
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
+                            })
+                        )
+                    })
+                )
+    }
+}