From d47c536f973f48d35081c4dbbfbd2b13394d9aa7 Mon Sep 17 00:00:00 2001 From: Vika Date: Wed, 27 Jul 2022 11:14:04 +0300 Subject: kittybox-templates: split out MF2 rendering and get rid of log crate Kittybox now uses tracing instead of log. Why would I keep an unneccesary dependency in my Cargo.lock? --- kittybox-rs/templates/Cargo.toml | 9 +- kittybox-rs/templates/src/lib.rs | 18 +- kittybox-rs/templates/src/mf2.rs | 409 ++++++++++++++++++++++++++++++++ kittybox-rs/templates/src/templates.rs | 414 +-------------------------------- 4 files changed, 430 insertions(+), 420 deletions(-) create mode 100644 kittybox-rs/templates/src/mf2.rs (limited to 'kittybox-rs/templates') diff --git a/kittybox-rs/templates/Cargo.toml b/kittybox-rs/templates/Cargo.toml index fe8ac19..a32a3a2 100644 --- a/kittybox-rs/templates/Cargo.toml +++ b/kittybox-rs/templates/Cargo.toml @@ -8,19 +8,20 @@ edition = "2021" [dev-dependencies] faker_rand = "^0.1.1" rand = "^0.8.5" -test-logger = "^0.1.0" [dev-dependencies.microformats] version="^0.2.0" [dependencies] ellipse = "^0.2.0" # Truncate and ellipsize strings in a human-friendly way http = "^0.2.7" # Hyper's strong HTTP types -log = "^0.4.14" # A lightweight logging facade for Rust markup = "^0.12.0" # HTML templating engine serde_json = "^1.0.64" # A JSON serialization file format -[dependencies.chrono] # Date and time library for Rust +[dependencies.chrono] version = "^0.4.19" features = ["serde"] [dependencies.kittybox-util] version = "0.1.0" -path = "../util" \ No newline at end of file +path = "../util" +[dependencies.kittybox-indieauth] +version = "0.1.0" +path = "../indieauth" \ No newline at end of file diff --git a/kittybox-rs/templates/src/lib.rs b/kittybox-rs/templates/src/lib.rs index 8e9abba..d58e831 100644 --- a/kittybox-rs/templates/src/lib.rs +++ b/kittybox-rs/templates/src/lib.rs @@ -1,9 +1,13 @@ mod templates; -pub use templates::{Entry, ErrorPage, Feed, MainPage, Template, VCard, POSTS_PER_PAGE}; +pub use templates::{ErrorPage, MainPage, Template}; mod onboarding; pub use onboarding::OnboardingPage; +mod indieauth; +pub use indieauth::AuthorizationRequestPage; mod login; pub use login::LoginPage; +mod mf2; +pub use mf2::{Entry, VCard, Feed, Food, POSTS_PER_PAGE}; #[cfg(test)] mod tests { @@ -196,11 +200,9 @@ mod tests { #[test] #[ignore = "see https://gitlab.com/maxburon/microformats-parser/-/issues/7"] fn test_note() { - test_logger::ensure_env_logger_initialized(); - let mf2 = gen_random_post(&rand::random::().to_string(), PostType::Note); - let html = crate::templates::Entry { post: &mf2 }.to_string(); + let html = crate::mf2::Entry { post: &mf2 }.to_string(); let url: Url = mf2 .pointer("/properties/uid/0") @@ -231,10 +233,8 @@ mod tests { #[test] fn test_article() { - test_logger::ensure_env_logger_initialized(); - let mf2 = gen_random_post(&rand::random::().to_string(), PostType::Article); - let html = crate::templates::Entry { post: &mf2 }.to_string(); + let html = crate::mf2::Entry { post: &mf2 }.to_string(); let url: Url = mf2 .pointer("/properties/uid/0") .and_then(|i| i.as_str()) @@ -273,8 +273,6 @@ mod tests { #[test] fn test_like_of() { - test_logger::ensure_env_logger_initialized(); - for likeof in [ PostType::LikeOf(gen_random_post( &rand::random::().to_string(), @@ -294,7 +292,7 @@ mod tests { .and_then(|i| i.as_str()) .and_then(|u| u.parse().ok()) .unwrap(); - let html = crate::templates::Entry { post: &mf2 }.to_string(); + let html = crate::mf2::Entry { post: &mf2 }.to_string(); let parsed: Document = microformats::from_html(&html, url.clone()).unwrap(); if let Some(item) = parsed.items.get(0) { diff --git a/kittybox-rs/templates/src/mf2.rs b/kittybox-rs/templates/src/mf2.rs new file mode 100644 index 0000000..d9fc5e7 --- /dev/null +++ b/kittybox-rs/templates/src/mf2.rs @@ -0,0 +1,409 @@ +use ellipse::Ellipse; + +pub static POSTS_PER_PAGE: usize = 20; + +/// Return a pretty location specifier from a geo: URI. +fn decode_geo_uri(uri: &str) -> String { + if let Some(part) = uri.split(':').collect::>().get(1) { + if let Some(part) = part.split(';').next() { + let mut parts = part.split(','); + let lat = parts.next().unwrap(); + let lon = parts.next().unwrap(); + // TODO - format them as proper latitude and longitude + return format!("{}, {}", lat, lon); + } else { + uri.to_string() + } + } else { + uri.to_string() + } +} + +markup::define! { + Entry<'a>(post: &'a serde_json::Value) { + @if post.pointer("/properties/like-of").is_none() && post.pointer("/properties/bookmark-of").is_none() { + @FullEntry { post } + } else { + // Show a mini-post. + @MiniEntry { post } + } + } + MiniEntry<'a>(post: &'a serde_json::Value) { + article."h-entry mini-entry" { + @if let Some(author) = post["properties"]["author"][0].as_object() { + span."mini-h-card"."u-author" { + a."u-author"[href=author["properties"]["uid"][0].as_str().unwrap()] { + @if let Some(photo) = author["properties"]["photo"][0].as_str() { + img[src=photo, loading="lazy"]; + } + @author["properties"]["name"][0].as_str().unwrap() + } + } + @if let Some(likeof) = post["properties"]["like-of"][0].as_str() { + " ❤️ " + a."u-like-of"[href=likeof] { @likeof } + } else if let Some(likeof) = post["properties"]["like-of"][0].as_object() { + a."u-like-of"[href=likeof["properties"]["url"][0].as_str().unwrap()] { + @likeof["properties"]["name"][0] + .as_str() + .unwrap_or_else(|| likeof["properties"]["url"][0].as_str().unwrap()) + } + } + @if let Some(bookmarkof) = post["properties"]["bookmark-of"][0].as_str() { + " 🔖 " + a."u-bookmark-of"[href=bookmarkof] { @bookmarkof } + } else if let Some(bookmarkof) = post["properties"]["bookmark-of"][0].as_object() { + a."u-bookmark-of"[href=bookmarkof["properties"]["url"][0].as_str().unwrap()] { + @bookmarkof["properties"]["name"][0] + .as_str() + .unwrap_or_else(|| bookmarkof["properties"]["url"][0].as_str().unwrap()) + } + } + @if let Some(published) = post["properties"]["published"][0].as_str() { + time."dt-published"[datetime=published] { + @chrono::DateTime::parse_from_rfc3339(published) + .map(|dt| dt.format("on %a %b %e %T %Y").to_string()) + .unwrap_or("sometime in the past".to_string()) + } + } + } + } + } + FullEntry<'a>(post: &'a serde_json::Value) { + article."h-entry" { + header.metadata { + @if let Some(name) = post["properties"]["name"][0].as_str() { + h1."p-name" { @name } + } + @if let Some(author) = post["properties"]["author"][0].as_object() { + section."mini-h-card" { + a.larger."u-author"[href=author["properties"]["uid"][0].as_str().unwrap()] { + @if let Some(photo) = author["properties"]["photo"][0].as_str() { + img[src=photo, loading="lazy"]; + } + @author["properties"]["name"][0].as_str().unwrap() + } + } + } + div { + span { + a."u-url"."u-uid"[href=post["properties"]["uid"][0].as_str().unwrap()] { + @if let Some(published) = post["properties"]["published"][0].as_str() { + time."dt-published"[datetime=published] { + @chrono::DateTime::parse_from_rfc3339(published) + .map(|dt| dt.format("%a %b %e %T %Y").to_string()) + .unwrap_or("sometime in the past".to_string()) + } + } + } + } + @if post["properties"]["visibility"][0].as_str().unwrap_or("public") != "public" { + span."p-visibility"[value=post["properties"]["visibility"][0].as_str().unwrap()] { + @post["properties"]["visibility"][0].as_str().unwrap() + } + } + @if post["properties"]["category"].is_array() { + span { + ul.categories { + "Tagged: " + @for cat in post["properties"]["category"].as_array().unwrap() { + li."p-category" { @cat.as_str().unwrap() } + } + } + } + } + @if post["properties"]["in-reply-to"].is_array() { + // TODO: Rich reply contexts - blocked on MF2 parser + span { + "In reply to: " + ul.replyctx { + @for ctx in post["properties"]["in-reply-to"].as_array().unwrap() { + li { a."u-in-reply-to"[href=ctx.as_str().unwrap()] { + @ctx.as_str().unwrap().truncate_ellipse(24).as_ref() + } } + } + } + } + } + } + @if post["properties"]["url"].as_array().unwrap().len() > 1 { + hr; + ul { + "Pretty permalinks for this post:" + @for url in post["properties"]["url"].as_array().unwrap().iter().filter(|i| **i != post["properties"]["uid"][0]).map(|i| i.as_str().unwrap()) { + li { + a."u-url"[href=url] { @url } + } + } + } + } + @if post["properties"]["location"].is_array() || post["properties"]["checkin"].is_array() { + div { + @if post["properties"]["checkin"].is_array() { + span { + "Check-in to: " + @if post["properties"]["checkin"][0].is_string() { + // It's a URL + a."u-checkin"[href=post["properties"]["checkin"][0].as_str().unwrap()] { + @post["properties"]["checkin"][0].as_str().unwrap().truncate_ellipse(24).as_ref() + } + } else { + a."u-checkin"[href=post["properties"]["checkin"][0]["properties"]["uid"][0].as_str().unwrap()] { + @post["properties"]["checkin"][0]["properties"]["name"][0].as_str().unwrap() + } + } + } + } + @if post["properties"]["location"].is_array() { + span { + "Location: " + @if post["properties"]["location"][0].is_string() { + // It's a geo: URL + // We need to decode it + a."u-location"[href=post["properties"]["location"][0].as_str().unwrap()] { + @decode_geo_uri(post["properties"]["location"][0].as_str().unwrap()) + } + } else { + // It's an inner h-geo object + a."u-location"[href=post["properties"]["location"][0]["value"].as_str().map(|x| x.to_string()).unwrap_or(format!("geo:{},{}", post["properties"]["location"][0]["properties"]["latitude"][0].as_str().unwrap(), post["properties"]["location"][0]["properties"]["longitude"][0].as_str().unwrap()))] { + // I'm a lazy bitch + @decode_geo_uri(&post["properties"]["location"][0]["value"].as_str().map(|x| x.to_string()).unwrap_or(format!("geo:{},{}", post["properties"]["location"][0]["properties"]["latitude"][0].as_str().unwrap(), post["properties"]["location"][0]["properties"]["longitude"][0].as_str().unwrap()))) + } + } + } + } + } + } + @if post["properties"]["ate"].is_array() || post["properties"]["drank"].is_array() { + div { + @if post["properties"]["ate"].is_array() { + span { ul { + "Ate:" + @for food in post["properties"]["ate"].as_array().unwrap() { + li { + @if food.is_string() { + // If this is a string, it's a URL. + a."u-ate"[href=food.as_str().unwrap()] { + @food.as_str().unwrap().truncate_ellipse(24).as_ref() + } + } else { + // This is a rich food object (mm, sounds tasty! I wanna eat something tasty) + a."u-ate"[href=food["properties"]["uid"][0].as_str().unwrap_or("#")] { + @food["properties"]["name"][0].as_str() + .unwrap_or(food["properties"]["uid"][0].as_str().unwrap_or("#").truncate_ellipse(24).as_ref()) + } + } + } + } + } } + } + @if post["properties"]["drank"].is_array() { + span { ul { + "Drank:" + @for food in post["properties"]["drank"].as_array().unwrap() { + li { + @if food.is_string() { + // If this is a string, it's a URL. + a."u-drank"[href=food.as_str().unwrap()] { + @food.as_str().unwrap().truncate_ellipse(24).as_ref() + } + } else { + // This is a rich food object (mm, sounds tasty! I wanna eat something tasty) + a."u-drank"[href=food["properties"]["uid"][0].as_str().unwrap_or("#")] { + @food["properties"]["name"][0].as_str() + .unwrap_or(food["properties"]["uid"][0].as_str().unwrap_or("#").truncate_ellipse(24).as_ref()) + } + } + } + } + } } + } + } + } + } + @PhotoGallery { photos: post["properties"]["photo"].as_array() } + @if post["properties"]["content"][0]["html"].is_string() { + main."e-content" { + @markup::raw(post["properties"]["content"][0]["html"].as_str().unwrap().trim()) + } + } + @WebInteractions { post } + } + } + VCard<'a>(card: &'a serde_json::Value) { + article."h-card" { + @if card["properties"]["photo"][0].is_string() { + img."u-photo"[src=card["properties"]["photo"][0].as_str().unwrap()]; + } + h1 { + a."u-url"."u-uid"."p-name"[href=card["properties"]["uid"][0].as_str().unwrap()] { + @card["properties"]["name"][0].as_str().unwrap() + } + } + @if card["properties"]["pronoun"].is_array() { + span { + "(" + @for (i, pronoun) in card["properties"]["pronoun"].as_array().unwrap().iter().filter_map(|v| v.as_str()).enumerate() { + span."p-pronoun" { + @pronoun + } + // Insert commas between multiple sets of pronouns + @if i < (card["properties"]["pronoun"].as_array().unwrap().len() - 1) {", "} + } + ")" + } + } + @if card["properties"]["note"].is_array() { + p."p-note" { + @card["properties"]["note"][0]["value"].as_str().unwrap_or_else(|| card["properties"]["note"][0].as_str().unwrap()) + } + } + @if card["properties"]["url"].is_array() { + ul { + "Can be found elsewhere at:" + @for url in card["properties"]["url"].as_array().unwrap().iter().filter_map(|v| v.as_str()).filter(|v| v != &card["properties"]["uid"][0].as_str().unwrap()).filter(|v| !v.starts_with(&card["properties"]["author"][0].as_str().unwrap())) { + li { a."u-url"[href=url, rel="me"] { @url } } + } + } + } + } + } + Food<'a>(food: &'a serde_json::Value) { + article."h-food" { + header.metadata { + h1 { + a."p-name"."u-url"[href=food["properties"]["url"][0].as_str().unwrap()] { + @food["properties"]["name"][0].as_str().unwrap() + } + } + } + @PhotoGallery { photos: food["properties"]["photo"].as_array() } + } + } + Feed<'a>(feed: &'a serde_json::Value) { + div."h-feed" { + div.metadata { + @if feed["properties"]["name"][0].is_string() { + h1."p-name".titanic { + a[href=feed["properties"]["uid"][0].as_str().unwrap(), rel="feed"] { + @feed["properties"]["name"][0].as_str().unwrap() + } + } + } + } + @if feed["children"].is_array() { + @for child in feed["children"].as_array().unwrap() { + @match child["type"][0].as_str().unwrap() { + "h-entry" => { @Entry { post: child } } + "h-feed" => { @Feed { feed: child } } + "h-food" => { @Food { food: child } } + //"h-event" => { } + "h-card" => { @VCard { card: child } } + something_else => { + p { + "There's supposed to be an " + @something_else + " object here. But Kittybox can't render it right now." + small { "Sorry! TToTT" } + } + } + } + } + } + @if feed["children"].as_array().map(|a| a.len()).unwrap_or(0) == 0 { + p { + "Looks like you reached the end. Wanna jump back to the " + a[href=feed["properties"]["uid"][0].as_str().unwrap()] { + "beginning" + } "?" + } + } + @if feed["children"].as_array().map(|a| a.len()).unwrap_or(0) == super::POSTS_PER_PAGE { + a[rel="prev", href=feed["properties"]["uid"][0].as_str().unwrap().to_string() + + "?after=" + feed["children"][super::POSTS_PER_PAGE - 1]["properties"]["uid"][0].as_str().unwrap()] { + "Older posts" + } + } + } + } + + //======================================= + // Components library + //======================================= + PhotoGallery<'a>(photos: Option<&'a Vec>) { + @if let Some(photos) = photos { + @for photo in photos.iter() { + @if let Some(photo) = photo.as_str() { + img."u-photo"[src=photo, loading="lazy"]; + } else if photo.is_object() { + @if let Some(thumbnail) = photo["thumbnail"].as_str() { + a."u-photo"[href=photo["value"].as_str().unwrap()] { + img[src=thumbnail, + loading="lazy", + alt=photo["alt"].as_str().unwrap_or("") + ]; + } + } else { + img."u-photo"[src=photo["value"].as_str().unwrap(), + loading="lazy", + alt=photo["alt"].as_str().unwrap_or("") + ]; + } + } + } + } + } + WebInteractions<'a>(post: &'a serde_json::Value) { + footer.webinteractions { + ul.counters { + li { + span.icon { "❤️" } + span.counter { @post["properties"]["like"].as_array().map(|a| a.len()).unwrap_or(0) } + } + li { + span.icon { "💬" } + span.counter { @post["properties"]["comment"].as_array().map(|a| a.len()).unwrap_or(0) } + } + li { + span.icon { "🔄" } + span.counter { @post["properties"]["repost"].as_array().map(|a| a.len()).unwrap_or(0) } + } + li { + span.icon { "🔖" } + span.counter { @post["properties"]["bookmark"].as_array().map(|a| a.len()).unwrap_or(0) } + } + } + /*@if ( + post["properties"]["like"].as_array().map(|a| a.len()).unwrap_or(0) + + post["properties"]["bookmark"].as_array().map(|a| a.len()).unwrap_or(0) + + post["properties"]["repost"].as_array().map(|a| a.len()).unwrap_or(0) + + post["properties"]["comment"].as_array().map(|a| a.len()).unwrap_or(0) + ) > 0 { + details { + summary { "Show comments and reactions" } + // TODO actually render facepiles and comments + @if let Some(likes) = post["properties"]["like"].as_array() { + @if !likes.is_empty() { + // Show a facepile of likes for a post + } + } + @if let Some(bookmarks) = post["properties"]["bookmark"].as_array() { + @if !bookmarks.is_empty() { + // Show a facepile of bookmarks for a post + } + } + @if let Some(reposts) = post["properties"]["repost"].as_array() { + @if !reposts.is_empty() { + // Show a facepile of reposts for a post + } + } + @if let Some(comments) = post["properties"]["comment"].as_array() { + @for comment in comments.iter() { + // Show all the comments recursively (so we could do Salmention with them) + } + } + } + }*/ + } + } +} diff --git a/kittybox-rs/templates/src/templates.rs b/kittybox-rs/templates/src/templates.rs index 0b1db28..60da6af 100644 --- a/kittybox-rs/templates/src/templates.rs +++ b/kittybox-rs/templates/src/templates.rs @@ -1,29 +1,10 @@ -use ellipse::Ellipse; use http::StatusCode; -use kittybox_util::{IndiewebEndpoints, MicropubChannel}; -use log::error; +use kittybox_util::MicropubChannel; -pub static POSTS_PER_PAGE: usize = 20; - -/// Return a pretty location specifier from a geo: URI. -fn decode_geo_uri(uri: &str) -> String { - if let Some(part) = uri.split(':').collect::>().get(1) { - if let Some(part) = part.split(';').next() { - let mut parts = part.split(','); - let lat = parts.next().unwrap(); - let lon = parts.next().unwrap(); - // TODO - format them as proper latitude and longitude - return format!("{}, {}", lat, lon); - } else { - uri.to_string() - } - } else { - uri.to_string() - } -} +use crate::{Feed, VCard}; markup::define! { - Template<'a>(title: &'a str, blog_name: &'a str, endpoints: Option, feeds: Vec, user: Option, content: String) { + Template<'a>(title: &'a str, blog_name: &'a str, feeds: Vec, user: Option, content: String) { @markup::doctype() html { head { @@ -31,20 +12,19 @@ markup::define! { link[rel="preconnect", href="https://fonts.gstatic.com"]; link[rel="stylesheet", href="/.kittybox/static/style.css"]; meta[name="viewport", content="initial-scale=1, width=device-width"]; - // Static, because it's built into the server itself + link[rel="micropub", href="/.kittybox/micropub"]; link[rel="micropub_media", href="/.kittybox/media"]; - // TODO change this once neccesary endpoints are implemented - @if let Some(endpoints) = endpoints { - link[rel="authorization_endpoint", href=&endpoints.authorization_endpoint]; - link[rel="token_endpoint", href=&endpoints.token_endpoint]; + link[rel="indieauth_metadata", href="/.kittybox/indieauth/metadata"]; + + /*@if let Some(endpoints) = endpoints { @if let Some(webmention) = &endpoints.webmention { link[rel="webmention", href=&webmention]; } @if let Some(microsub) = &endpoints.microsub { link[rel="microsub", href=µsub]; } - } + }*/ } body { // TODO Somehow compress headerbar into a menu when the screen space is tight @@ -78,384 +58,6 @@ markup::define! { } } } - Entry<'a>(post: &'a serde_json::Value) { - @if post.pointer("/properties/like-of").is_none() && post.pointer("/properties/bookmark-of").is_none() { - @FullEntry { post } - } else { - // Show a mini-post. - @MiniEntry { post } - } - } - MiniEntry<'a>(post: &'a serde_json::Value) { - article."h-entry mini-entry" { - @if let Some(author) = post["properties"]["author"][0].as_object() { - span."mini-h-card"."u-author" { - a."u-author"[href=author["properties"]["uid"][0].as_str().unwrap()] { - @if let Some(photo) = author["properties"]["photo"][0].as_str() { - img[src=photo, loading="lazy"]; - } - @author["properties"]["name"][0].as_str().unwrap() - } - } - @if let Some(likeof) = post["properties"]["like-of"][0].as_str() { - " ❤️ " - a."u-like-of"[href=likeof] { @likeof } - } else if let Some(likeof) = post["properties"]["like-of"][0].as_object() { - a."u-like-of"[href=likeof["properties"]["url"][0].as_str().unwrap()] { - @likeof["properties"]["name"][0] - .as_str() - .unwrap_or_else(|| likeof["properties"]["url"][0].as_str().unwrap()) - } - } - @if let Some(bookmarkof) = post["properties"]["bookmark-of"][0].as_str() { - " 🔖 " - a."u-bookmark-of"[href=bookmarkof] { @bookmarkof } - } else if let Some(bookmarkof) = post["properties"]["bookmark-of"][0].as_object() { - a."u-bookmark-of"[href=bookmarkof["properties"]["url"][0].as_str().unwrap()] { - @bookmarkof["properties"]["name"][0] - .as_str() - .unwrap_or_else(|| bookmarkof["properties"]["url"][0].as_str().unwrap()) - } - } - @if let Some(published) = post["properties"]["published"][0].as_str() { - time."dt-published"[datetime=published] { - @chrono::DateTime::parse_from_rfc3339(published) - .map(|dt| dt.format("on %a %b %e %T %Y").to_string()) - .unwrap_or("sometime in the past".to_string()) - } - } - } - } - } - FullEntry<'a>(post: &'a serde_json::Value) { - article."h-entry" { - header.metadata { - @if let Some(name) = post["properties"]["name"][0].as_str() { - h1."p-name" { @name } - } - @if let Some(author) = post["properties"]["author"][0].as_object() { - section."mini-h-card" { - a.larger."u-author"[href=author["properties"]["uid"][0].as_str().unwrap()] { - @if let Some(photo) = author["properties"]["photo"][0].as_str() { - img[src=photo, loading="lazy"]; - } - @author["properties"]["name"][0].as_str().unwrap() - } - } - } - div { - span { - a."u-url"."u-uid"[href=post["properties"]["uid"][0].as_str().unwrap()] { - @if let Some(published) = post["properties"]["published"][0].as_str() { - time."dt-published"[datetime=published] { - @chrono::DateTime::parse_from_rfc3339(published) - .map(|dt| dt.format("%a %b %e %T %Y").to_string()) - .unwrap_or("sometime in the past".to_string()) - } - } - } - } - @if post["properties"]["visibility"][0].as_str().unwrap_or("public") != "public" { - span."p-visibility"[value=post["properties"]["visibility"][0].as_str().unwrap()] { - @post["properties"]["visibility"][0].as_str().unwrap() - } - } - @if post["properties"]["category"].is_array() { - span { - ul.categories { - "Tagged: " - @for cat in post["properties"]["category"].as_array().unwrap() { - li."p-category" { @cat.as_str().unwrap() } - } - } - } - } - @if post["properties"]["in-reply-to"].is_array() { - // TODO: Rich reply contexts - blocked on MF2 parser - span { - "In reply to: " - ul.replyctx { - @for ctx in post["properties"]["in-reply-to"].as_array().unwrap() { - li { a."u-in-reply-to"[href=ctx.as_str().unwrap()] { - @ctx.as_str().unwrap().truncate_ellipse(24).as_ref() - } } - } - } - } - } - } - @if post["properties"]["url"].as_array().unwrap().len() > 1 { - hr; - ul { - "Pretty permalinks for this post:" - @for url in post["properties"]["url"].as_array().unwrap().iter().filter(|i| **i != post["properties"]["uid"][0]).map(|i| i.as_str().unwrap()) { - li { - a."u-url"[href=url] { @url } - } - } - } - } - @if post["properties"]["location"].is_array() || post["properties"]["checkin"].is_array() { - div { - @if post["properties"]["checkin"].is_array() { - span { - "Check-in to: " - @if post["properties"]["checkin"][0].is_string() { - // It's a URL - a."u-checkin"[href=post["properties"]["checkin"][0].as_str().unwrap()] { - @post["properties"]["checkin"][0].as_str().unwrap().truncate_ellipse(24).as_ref() - } - } else { - a."u-checkin"[href=post["properties"]["checkin"][0]["properties"]["uid"][0].as_str().unwrap()] { - @post["properties"]["checkin"][0]["properties"]["name"][0].as_str().unwrap() - } - } - } - } - @if post["properties"]["location"].is_array() { - span { - "Location: " - @if post["properties"]["location"][0].is_string() { - // It's a geo: URL - // We need to decode it - a."u-location"[href=post["properties"]["location"][0].as_str().unwrap()] { - @decode_geo_uri(post["properties"]["location"][0].as_str().unwrap()) - } - } else { - // It's an inner h-geo object - a."u-location"[href=post["properties"]["location"][0]["value"].as_str().map(|x| x.to_string()).unwrap_or(format!("geo:{},{}", post["properties"]["location"][0]["properties"]["latitude"][0].as_str().unwrap(), post["properties"]["location"][0]["properties"]["longitude"][0].as_str().unwrap()))] { - // I'm a lazy bitch - @decode_geo_uri(&post["properties"]["location"][0]["value"].as_str().map(|x| x.to_string()).unwrap_or(format!("geo:{},{}", post["properties"]["location"][0]["properties"]["latitude"][0].as_str().unwrap(), post["properties"]["location"][0]["properties"]["longitude"][0].as_str().unwrap()))) - } - } - } - } - } - } - @if post["properties"]["ate"].is_array() || post["properties"]["drank"].is_array() { - div { - @if post["properties"]["ate"].is_array() { - span { ul { - "Ate:" - @for food in post["properties"]["ate"].as_array().unwrap() { - li { - @if food.is_string() { - // If this is a string, it's a URL. - a."u-ate"[href=food.as_str().unwrap()] { - @food.as_str().unwrap().truncate_ellipse(24).as_ref() - } - } else { - // This is a rich food object (mm, sounds tasty! I wanna eat something tasty) - a."u-ate"[href=food["properties"]["uid"][0].as_str().unwrap_or("#")] { - @food["properties"]["name"][0].as_str() - .unwrap_or(food["properties"]["uid"][0].as_str().unwrap_or("#").truncate_ellipse(24).as_ref()) - } - } - } - } - } } - } - @if post["properties"]["drank"].is_array() { - span { ul { - "Drank:" - @for food in post["properties"]["drank"].as_array().unwrap() { - li { - @if food.is_string() { - // If this is a string, it's a URL. - a."u-drank"[href=food.as_str().unwrap()] { - @food.as_str().unwrap().truncate_ellipse(24).as_ref() - } - } else { - // This is a rich food object (mm, sounds tasty! I wanna eat something tasty) - a."u-drank"[href=food["properties"]["uid"][0].as_str().unwrap_or("#")] { - @food["properties"]["name"][0].as_str() - .unwrap_or(food["properties"]["uid"][0].as_str().unwrap_or("#").truncate_ellipse(24).as_ref()) - } - } - } - } - } } - } - } - } - } - @PhotoGallery { photos: post["properties"]["photo"].as_array() } - @if post["properties"]["content"][0]["html"].is_string() { - main."e-content" { - @markup::raw(post["properties"]["content"][0]["html"].as_str().unwrap().trim()) - } - } - @WebInteractions { post } - } - } - PhotoGallery<'a>(photos: Option<&'a Vec>) { - @if let Some(photos) = photos { - @for photo in photos.iter() { - @if let Some(photo) = photo.as_str() { - img."u-photo"[src=photo, loading="lazy"]; - } else if photo.is_object() { - @if let Some(thumbnail) = photo["thumbnail"].as_str() { - a."u-photo"[href=photo["value"].as_str().unwrap()] { - img[src=thumbnail, - loading="lazy", - alt=photo["alt"].as_str().unwrap_or("") - ]; - } - } else { - img."u-photo"[src=photo["value"].as_str().unwrap(), - loading="lazy", - alt=photo["alt"].as_str().unwrap_or("") - ]; - } - } - } - } - } - WebInteractions<'a>(post: &'a serde_json::Value) { - footer.webinteractions { - ul.counters { - li { - span.icon { "❤️" } - span.counter { @post["properties"]["like"].as_array().map(|a| a.len()).unwrap_or(0) } - } - li { - span.icon { "💬" } - span.counter { @post["properties"]["comment"].as_array().map(|a| a.len()).unwrap_or(0) } - } - li { - span.icon { "🔄" } - span.counter { @post["properties"]["repost"].as_array().map(|a| a.len()).unwrap_or(0) } - } - li { - span.icon { "🔖" } - span.counter { @post["properties"]["bookmark"].as_array().map(|a| a.len()).unwrap_or(0) } - } - } - /*@if ( - post["properties"]["like"].as_array().map(|a| a.len()).unwrap_or(0) - + post["properties"]["bookmark"].as_array().map(|a| a.len()).unwrap_or(0) - + post["properties"]["repost"].as_array().map(|a| a.len()).unwrap_or(0) - + post["properties"]["comment"].as_array().map(|a| a.len()).unwrap_or(0) - ) > 0 { - details { - summary { "Show comments and reactions" } - // TODO actually render facepiles and comments - @if let Some(likes) = post["properties"]["like"].as_array() { - @if !likes.is_empty() { - // Show a facepile of likes for a post - } - } - @if let Some(bookmarks) = post["properties"]["bookmark"].as_array() { - @if !bookmarks.is_empty() { - // Show a facepile of bookmarks for a post - } - } - @if let Some(reposts) = post["properties"]["repost"].as_array() { - @if !reposts.is_empty() { - // Show a facepile of reposts for a post - } - } - @if let Some(comments) = post["properties"]["comment"].as_array() { - @for comment in comments.iter() { - // Show all the comments recursively (so we could do Salmention with them) - } - } - } - }*/ - } - } - VCard<'a>(card: &'a serde_json::Value) { - article."h-card" { - @if card["properties"]["photo"][0].is_string() { - img."u-photo"[src=card["properties"]["photo"][0].as_str().unwrap()]; - } - h1 { - a."u-url"."u-uid"."p-name"[href=card["properties"]["uid"][0].as_str().unwrap()] { - @card["properties"]["name"][0].as_str().unwrap() - } - } - @if card["properties"]["pronoun"].is_array() { - span { - "(" - @for (i, pronoun) in card["properties"]["pronoun"].as_array().unwrap().iter().filter_map(|v| v.as_str()).enumerate() { - span."p-pronoun" { - @pronoun - } - // Insert commas between multiple sets of pronouns - @if i < (card["properties"]["pronoun"].as_array().unwrap().len() - 1) {", "} - } - ")" - } - } - @if card["properties"]["note"].is_array() { - p."p-note" { - @card["properties"]["note"][0]["value"].as_str().unwrap_or_else(|| card["properties"]["note"][0].as_str().unwrap()) - } - } - @if card["properties"]["url"].is_array() { - ul { - "Can be found elsewhere at:" - @for url in card["properties"]["url"].as_array().unwrap().iter().filter_map(|v| v.as_str()).filter(|v| v != &card["properties"]["uid"][0].as_str().unwrap()).filter(|v| !v.starts_with(&card["properties"]["author"][0].as_str().unwrap())) { - li { a."u-url"[href=url, rel="me"] { @url } } - } - } - } - } - } - Food<'a>(food: &'a serde_json::Value) { - article."h-food" { - header.metadata { - h1 { - a."p-name"."u-url"[href=food["properties"]["url"][0].as_str().unwrap()] { - @food["properties"]["name"][0].as_str().unwrap() - } - } - } - @PhotoGallery { photos: food["properties"]["photo"].as_array() } - } - } - Feed<'a>(feed: &'a serde_json::Value) { - div."h-feed" { - div.metadata { - @if feed["properties"]["name"][0].is_string() { - h1."p-name".titanic { - a[href=feed["properties"]["uid"][0].as_str().unwrap(), rel="feed"] { - @feed["properties"]["name"][0].as_str().unwrap() - } - } - } - } - @if feed["children"].is_array() { - @for child in feed["children"].as_array().unwrap() { - @match child["type"][0].as_str().unwrap() { - "h-entry" => { @Entry { post: child } } - "h-feed" => { @Feed { feed: child } } - "h-event" => { - @{error!("Templating error: h-events aren't implemented yet");} - } - "h-card" => { @VCard { card: child }} - something_else => { - @{error!("Templating error: found a {} object that couldn't be parsed", something_else);} - } - } - } - } - @if feed["children"].as_array().map(|a| a.len()).unwrap_or(0) == 0 { - p { - "Looks like you reached the end. Wanna jump back to the " - a[href=feed["properties"]["uid"][0].as_str().unwrap()] { - "beginning" - } "?" - } - } - @if feed["children"].as_array().map(|a| a.len()).unwrap_or(0) == super::POSTS_PER_PAGE { - a[rel="prev", href=feed["properties"]["uid"][0].as_str().unwrap().to_string() - + "?after=" + feed["children"][super::POSTS_PER_PAGE - 1]["properties"]["uid"][0].as_str().unwrap()] { - "Older posts" - } - } - } - } MainPage<'a>(feed: &'a serde_json::Value, card: &'a serde_json::Value) { .sidebyside { @VCard { card } -- cgit 1.4.1