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 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"]; } else if author["properties"]["photo"][0].is_object() { img[ src=author["properties"]["photo"][0]["value"].as_str().unwrap(), alt=author["properties"]["photo"][0]["alt"].as_str().unwrap(), loading="lazy" ]; } @author["properties"]["name"][0].as_str().unwrap() } } @if let Some(likeof) = post["properties"]["like-of"][0].as_str() { " " span."like-icon"["aria-label"="liked"] { span."like-icon-label"["aria-hidden"="true"] { "❤️" } } " " 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()) } } " " 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("on %a %b %e %T %Y").to_string()) .unwrap_or("sometime in the past".to_string()) } } else { "sometime in the past" } } } } } 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"]; } else if let Some(photo) = author["properties"]["photo"][0].as_object() { img[ src=photo["value"].as_str().unwrap(), alt=photo["alt"].as_str().unwrap(), 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() { span { "In reply to: " ul.replyctx { @for ctx in post["properties"]["in-reply-to"].as_array().unwrap() { @if let Some(ctx) = ctx.as_str() { li { a."u-in-reply-to"[href=ctx] { @ctx.truncate_ellipse(48).as_ref() } } } else if let Some(ctx) = ctx.as_object() { li { a."u-in-reply-to"[href=ctx["properties"]["uid"][0] .as_str() .unwrap_or_else(|| ctx["properties"]["url"][0].as_str().unwrap())] { @ctx["properties"]["uid"][0] .as_str() .unwrap_or_else(|| ctx["properties"]["url"][0].as_str().unwrap()) .truncate_ellipse(48) .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 let Some(links) = post["properties"]["syndication"].as_array() { @if !links.is_empty() { hr; ul { "Also published on:" @for url in links.iter().filter_map(|i| i.as_str()) { li { a."u-syndication"[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()]; } else if card["properties"]["photo"][0].is_object() { img."u-photo"[ src=card["properties"]["photo"][0]["value"].as_str().unwrap(), alt=card["properties"]["photo"][0]["alt"].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, cursor: Option<&'a str>) { 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, cursor: None } } "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 { p[style="display: none", "aria-hidden"="false"] { "Webmention counters:" } ul.counters { li { span."icon like-icon"["aria-label"="likes"] { span."like-icon-label"["aria-hidden"="true"] { "❤️" } } span.counter { @post["properties"]["like"].as_array().map(|a| a.len()).unwrap_or(0) } } li { span.icon["aria-label"="replies"] { "💬" } span.counter { @post["properties"]["comment"].as_array().map(|a| a.len()).unwrap_or(0) } } li { span.icon["aria-label"="reposts"] { "🔄" } span.counter { @post["properties"]["repost"].as_array().map(|a| a.len()).unwrap_or(0) } } li { span.icon["aria-label"="bookmarks"] { "🔖" } 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) } } } }*/ } } }