ogp
All checks were successful
ci/woodpecker/push/test Pipeline was successful

This commit is contained in:
JMARyA 2025-02-23 22:52:19 +01:00
parent b8ed8da199
commit 696b34f2f1
Signed by: jmarya
GPG key ID: 901B2ADDF27C2263
4 changed files with 669 additions and 2 deletions

1
.gitignore vendored
View file

@ -3,3 +3,4 @@ src/htmx.min.js
src/flowbite.min.css
src/flowbite.min.js
src/material.woff2
/examples/test.rs

View file

@ -6,6 +6,7 @@ use tokio::sync::OnceCell;
pub mod asset;
pub mod auth;
pub mod format;
pub mod ogp;
pub mod request;
pub mod result;
pub mod ui;

656
src/ogp.rs Normal file
View file

@ -0,0 +1,656 @@
use maud::{PreEscaped, html};
use crate::ui::prelude::Optional;
// TODO : documentation
#[allow(non_snake_case)]
pub fn Meta(key: &str, value: &str) -> PreEscaped<String> {
html! {
meta property=(key) content=(value) {};
}
}
#[derive(Debug, Clone)]
pub struct Metadata {
pub title: String,
pub kind: PreEscaped<String>,
pub image: MediaItem,
pub url: String,
pub audio: Option<MediaItem>,
pub description: Option<String>,
pub determiner: Option<Determiner>,
pub locale: Option<String>,
pub locale_alternate: Vec<String>,
pub site_name: Option<String>,
pub video: Option<MediaItem>,
}
impl Metadata {
pub fn new<T: ObjectType>(url: &str, title: &str, image: MediaItem, kind: T) -> Self {
Self {
title: title.to_string(),
kind: kind.render(),
image,
url: url.to_string(),
audio: None,
description: None,
determiner: None,
locale: None,
locale_alternate: Vec::new(),
site_name: None,
video: None,
}
}
pub fn audio(mut self, audio: MediaItem) -> Self {
self.audio = Some(audio);
self
}
pub fn description(mut self, description: &str) -> Self {
self.description = Some(description.to_string());
self
}
pub fn determiner(mut self, determiner: Determiner) -> Self {
self.determiner = Some(determiner);
self
}
pub fn locale(mut self, locale: &str) -> Self {
self.locale = Some(locale.to_string());
self
}
pub fn locale_alternate(mut self, locale: &str) -> Self {
self.locale_alternate.push(locale.to_string());
self
}
pub fn site_name(mut self, site_name: &str) -> Self {
self.site_name = Some(site_name.to_string());
self
}
pub fn video(mut self, video: MediaItem) -> Self {
self.video = Some(video);
self
}
}
fn array_meta(v: &[String], tag_name: &str) -> PreEscaped<String> {
let r = v.into_iter().map(|x| Meta(tag_name, x)).collect::<Vec<_>>();
html! {
@for e in r {
(e)
}
}
}
#[derive(Debug, Clone)]
pub enum Determiner {
A,
An,
The,
Blank,
Auto,
}
impl Determiner {
pub fn render(&self) -> PreEscaped<String> {
match self {
Determiner::A => Meta("og:determiner", "a"),
Determiner::An => Meta("og:determiner", "an"),
Determiner::The => Meta("og:determiner", "the"),
Determiner::Blank => Meta("og:determiner", ""),
Determiner::Auto => Meta("og:determiner", "auto"),
}
}
}
impl Metadata {
pub fn render(&self) -> PreEscaped<String> {
html! {
(Meta("og:title", &self.title))
(Optional(self.description.as_ref(), |desc| Meta("og:description", desc)))
(Optional(self.site_name.as_ref(), |site_name| Meta("og:site_name", site_name)))
(Optional(self.determiner.as_ref(), |determiner| determiner.render()))
(Optional(self.locale.as_ref(), |locale| Meta("og:locale", locale)))
(array_meta(&self.locale_alternate, "og:locale:alternate"))
(Meta("og:url", &self.url))
(self.image.render())
(Optional(self.video.as_ref(), |video| video.render()))
(Optional(self.audio.as_ref(), |audio| audio.render()))
(self.kind)
}
}
}
#[derive(Debug, Clone)]
pub enum MediaItemKind {
Image,
Video,
Audio,
}
#[derive(Debug, Clone)]
pub struct MediaItem {
pub url: String,
pub secure_url: Option<String>,
pub mime: String,
pub width: Option<u32>,
pub height: Option<u32>,
pub alt: String,
pub kind: MediaItemKind,
}
impl MediaItem {
pub fn secure_url(mut self, url: &str) -> Self {
self.secure_url = Some(url.to_string());
self
}
pub fn width(mut self, width: u32) -> Self {
self.width = Some(width);
self
}
pub fn height(mut self, height: u32) -> Self {
self.height = Some(height);
self
}
}
impl MediaItem {
#[allow(non_snake_case)]
pub fn Image(url: &str, mime: &str, alt: &str) -> Self {
let secure_url = if url.starts_with("https") {
Some(url.to_string())
} else {
None
};
MediaItem {
url: url.to_string(),
secure_url: secure_url,
mime: mime.to_string(),
width: None,
height: None,
alt: alt.to_string(),
kind: MediaItemKind::Image,
}
}
#[allow(non_snake_case)]
pub fn Video(url: &str, mime: &str, alt: &str) -> Self {
let secure_url = if url.starts_with("https") {
Some(url.to_string())
} else {
None
};
MediaItem {
url: url.to_string(),
secure_url: secure_url,
mime: mime.to_string(),
width: None,
height: None,
alt: alt.to_string(),
kind: MediaItemKind::Video,
}
}
#[allow(non_snake_case)]
pub fn Audio(url: &str, mime: &str, alt: &str) -> Self {
let secure_url = if url.starts_with("https") {
Some(url.to_string())
} else {
None
};
MediaItem {
url: url.to_string(),
secure_url: secure_url,
mime: mime.to_string(),
width: None,
height: None,
alt: alt.to_string(),
kind: MediaItemKind::Audio,
}
}
}
impl MediaItem {
pub fn render(&self) -> PreEscaped<String> {
match self.kind {
MediaItemKind::Image => {
html! {
(Meta("og:image", &self.url))
(Optional(self.secure_url.as_ref(), |secure_url| Meta("og:image:secure_url", secure_url)))
(Meta("og:image:type", &self.mime))
(Optional(self.width.as_ref(), |width| Meta("og:image:width", &width.to_string())))
(Optional(self.height.as_ref(), |height| Meta("og:image:height", &height.to_string())))
(Meta("og:image:alt", &self.alt))
}
}
MediaItemKind::Video => {
html! {
(Meta("og:video", &self.url))
(Optional(self.secure_url.as_ref(), |secure_url| Meta("og:video:secure_url", secure_url)))
(Meta("og:video:type", &self.mime))
(Optional(self.width.as_ref(), |width| Meta("og:video:width", &width.to_string())))
(Optional(self.height.as_ref(), |height| Meta("og:video:height", &height.to_string())))
}
}
MediaItemKind::Audio => {
html! {
(Meta("og:audio", &self.url))
(Optional(self.secure_url.as_ref(), |secure_url| Meta("og:audio:secure_url", secure_url)))
(Meta("og:audio:type", &self.mime))
}
}
}
}
}
pub struct Song {
pub duration: u32,
pub album: String,
pub album_disc: u32,
pub album_track: u32,
pub musician: Vec<String>,
}
impl Song {
pub fn new(duration: u32, album: &str, album_disc: u32, album_track: u32) -> Self {
Self {
duration,
album: album.to_string(),
album_disc,
album_track,
musician: Vec::new(),
}
}
pub fn musician(mut self, musician: &str) -> Self {
self.musician.push(musician.to_string());
self
}
}
impl ObjectType for Song {
fn render(&self) -> PreEscaped<String> {
html! {
(Meta("og:type", "music.song"))
(Meta("music:duration", &self.duration.to_string()))
(Meta("music:album:disc", &self.album_disc.to_string()))
(Meta("music:album:track", &self.album_track.to_string()))
(array_meta(&self.musician, "music:musician"))
}
}
}
pub struct Album {
pub song: Vec<(String, u32, u32)>,
pub musician: String,
pub release_date: chrono::NaiveDate,
}
impl Album {
pub fn new(musician: &str, release_date: chrono::NaiveDate) -> Self {
Self {
song: Vec::new(),
musician: musician.to_string(),
release_date,
}
}
pub fn song(mut self, song: &str, disc: u32, track: u32) -> Self {
self.song.push((song.to_string(), disc, track));
self
}
}
impl ObjectType for Album {
fn render(&self) -> PreEscaped<String> {
html! {
@for song in &self.song {
(Meta("music:song", &song.0))
(Meta("music:song:disc", &song.1.to_string()))
(Meta("music:song:track", &song.2.to_string()))
}
(Meta("music:musician", &self.musician))
(Meta("music:release_date", &self.release_date.format("%Y-%m-%d").to_string()))
}
}
}
pub struct Playlist {
pub song: Vec<(String, u32, u32)>,
pub creator: String,
}
impl Playlist {
pub fn new(creator: &str) -> Self {
Self {
song: Vec::new(),
creator: creator.to_string(),
}
}
pub fn song(mut self, song: &str, disc: u32, track: u32) -> Self {
self.song.push((song.to_string(), disc, track));
self
}
}
impl ObjectType for Playlist {
fn render(&self) -> PreEscaped<String> {
html! {
@for song in &self.song {
(Meta("music:song", &song.0))
(Meta("music:song:disc", &song.1.to_string()))
(Meta("music:song:track", &song.2.to_string()))
}
(Meta("music:creator", &self.creator))
}
}
}
pub struct RadioStation {
pub creator: String,
}
impl RadioStation {
pub fn new(creator: &str) -> Self {
Self {
creator: creator.to_string(),
}
}
}
impl ObjectType for RadioStation {
fn render(&self) -> PreEscaped<String> {
html! {
(Meta("music:creator", &self.creator))
}
}
}
pub struct Video {
pub actor: Vec<(String, String)>,
pub director: Vec<String>,
pub writer: Vec<String>,
pub duration: u32,
pub release_date: chrono::NaiveDate,
pub tag: Vec<String>,
pub series: Option<String>,
pub kind: VideoType,
}
impl Video {
pub fn actor(mut self, actor: &str, role: &str) -> Self {
self.actor.push((actor.to_string(), role.to_string()));
self
}
pub fn director(mut self, director: &str) -> Self {
self.director.push(director.to_string());
self
}
pub fn writer(mut self, writer: &str) -> Self {
self.writer.push(writer.to_string());
self
}
pub fn tag(mut self, tag: &str) -> Self {
self.tag.push(tag.to_string());
self
}
}
impl Video {
#[allow(non_snake_case)]
pub fn Movie(duration: u32, release_date: chrono::NaiveDate) -> Self {
Self {
actor: Vec::new(),
director: Vec::new(),
writer: Vec::new(),
duration,
release_date,
tag: Vec::new(),
series: None,
kind: VideoType::Movie,
}
}
#[allow(non_snake_case)]
pub fn Episode(duration: u32, release_date: chrono::NaiveDate) -> Self {
Self {
actor: Vec::new(),
director: Vec::new(),
writer: Vec::new(),
duration,
release_date,
tag: Vec::new(),
series: None,
kind: VideoType::Episode,
}
}
#[allow(non_snake_case)]
pub fn TV_Show(duration: u32, release_date: chrono::NaiveDate, series: &str) -> Self {
Self {
actor: Vec::new(),
director: Vec::new(),
writer: Vec::new(),
duration,
release_date,
tag: Vec::new(),
series: Some(series.to_string()),
kind: VideoType::TV_Show,
}
}
#[allow(non_snake_case)]
pub fn Other(duration: u32, release_date: chrono::NaiveDate) -> Self {
Self {
actor: Vec::new(),
director: Vec::new(),
writer: Vec::new(),
duration,
release_date,
tag: Vec::new(),
series: None,
kind: VideoType::Other,
}
}
}
impl ObjectType for Video {
fn render(&self) -> PreEscaped<String> {
html! {
(Meta("og:type", self.kind.kind()))
@for actor in &self.actor {
(Meta("video:actor", &actor.0))
(Meta("video:actor:role", &actor.1))
}
(array_meta(&self.director, "video:director"))
(array_meta(&self.writer, "video:writer"))
(Meta("video:duration", &self.duration.to_string()))
(Meta("video:release_date", &self.release_date.format("%Y-%m-%d").to_string()))
(array_meta(&self.tag, "video:tag"))
(Optional(self.series.as_ref(), |series| Meta("video.series", series)))
}
}
}
pub enum VideoType {
Movie,
Episode,
#[allow(non_camel_case_types)]
TV_Show,
Other,
}
impl VideoType {
pub fn kind(&self) -> &str {
match self {
VideoType::Movie => "video.movie",
VideoType::Episode => "video.episode",
VideoType::TV_Show => "video.tv_show",
VideoType::Other => "video.other",
}
}
}
pub struct Article {
pub published_time: chrono::NaiveDate,
pub modified_time: chrono::NaiveDate,
pub expiration_time: chrono::NaiveDate,
pub author: Vec<String>,
pub section: String,
pub tag: Vec<String>,
}
impl Article {
pub fn new(
published_time: chrono::NaiveDate,
modified_time: chrono::NaiveDate,
expiration_time: chrono::NaiveDate,
section: &str,
) -> Self {
Self {
published_time,
modified_time,
expiration_time,
author: Vec::new(),
section: section.to_string(),
tag: Vec::new(),
}
}
pub fn author(mut self, author: &str) -> Self {
self.author.push(author.to_string());
self
}
pub fn tag(mut self, tag: &str) -> Self {
self.tag.push(tag.to_string());
self
}
}
impl ObjectType for Article {
fn render(&self) -> PreEscaped<String> {
html! {
(Meta("article:published_time", &self.published_time.format("%Y-%m-%d").to_string()))
(Meta("article:modified_time", &self.modified_time.format("%Y-%m-%d").to_string()))
(Meta("article:expiration_time", &self.expiration_time.format("%Y-%m-%d").to_string()))
(array_meta(&self.author, "article:author"))
(Meta("article:section", &self.section))
(array_meta(&self.tag, "article:tag"))
}
}
}
pub struct Book {
pub author: Vec<String>,
pub isbn: String,
pub release_date: chrono::NaiveDate,
pub tag: Vec<String>,
}
impl Book {
pub fn new(isbn: &str, release_date: chrono::NaiveDate) -> Self {
Self {
author: Vec::new(),
isbn: isbn.to_string(),
release_date,
tag: Vec::new(),
}
}
pub fn author(mut self, author: &str) -> Self {
self.author.push(author.to_string());
self
}
pub fn tag(mut self, tag: &str) -> Self {
self.tag.push(tag.to_string());
self
}
}
impl ObjectType for Book {
fn render(&self) -> PreEscaped<String> {
html! {
(array_meta(&self.author, "book:author"))
(Meta("book:isbn", &self.isbn))
(Meta("book:release_date", &self.release_date.format("%Y-%m-%d").to_string()))
(array_meta(&self.tag, "book:tag"))
}
}
}
pub struct Profile {
first_name: String,
last_name: String,
username: String,
gender: Gender,
}
impl Profile {
pub fn new(first_name: &str, last_name: &str, username: &str, gender: Gender) -> Self {
Self {
first_name: first_name.to_string(),
last_name: last_name.to_string(),
username: username.to_string(),
gender,
}
}
}
pub enum Gender {
Male,
Female,
None,
}
impl ObjectType for Profile {
fn render(&self) -> PreEscaped<String> {
html! {
(Meta("profile:first_name", &self.first_name))
(Meta("profile:last_name", &self.last_name))
(Meta("profile:username", &self.username))
(Meta("profile:gender", match self.gender {
Gender::Male => "male",
Gender::Female => "female",
Gender::None => "none",
}))
}
}
}
pub struct Website;
impl Website {
pub fn new() -> Self {
Self
}
}
impl ObjectType for Website {
fn render(&self) -> PreEscaped<String> {
html! {
(Meta("og:type", "website"))
}
}
}
pub trait ObjectType {
fn render(&self) -> PreEscaped<String>;
}

View file

@ -3,12 +3,13 @@ use std::sync::Arc;
use maud::{PreEscaped, Render, html};
use crate::{
ogp::Metadata,
request::{RequestContext, StringResponse},
ui::{
UIWidget,
color::{Gray, UIColor},
htmx::{Event, HTMXAttributes, Selector, SwapStrategy},
prelude::{Div, Link},
prelude::{Div, Link, Optional},
primitives::link::LinkWidget,
},
};
@ -25,6 +26,7 @@ pub struct Shell {
/// The HTML content for the static body portion.
body_content: Arc<PreEscaped<String>>,
ui: bool,
metadata: Option<Metadata>,
bottom_nav: Option<Arc<PreEscaped<String>>>,
sidebar: Option<Arc<SidebarWidget>>,
navbar: Option<Arc<NavBarWidget>>,
@ -51,6 +53,7 @@ impl Shell {
main_class: Arc::new(body_class.extended_class().join(" ")),
body_content: Arc::new(body_content.render()),
ui: false,
metadata: None,
bottom_nav: None,
sidebar: None,
navbar: None,
@ -69,6 +72,11 @@ impl Shell {
self.clone()
}
pub fn metadata(mut self, metadata: Metadata) -> Self {
self.metadata = Some(metadata);
self
}
pub fn with_navbar(mut self, navbar: NavBarWidget) -> Self {
self.navbar = Some(Arc::new(navbar));
self
@ -123,6 +131,7 @@ impl Shell {
meta name="viewport" content="width=device-width, initial-scale=1.0";
};
(self.head)
(Optional(self.metadata.as_ref(), |meta| meta.render()))
};