fluffle/src/model.rs

276 lines
9.3 KiB
Rust
Raw Normal View History

2025-07-20 02:49:42 -04:00
use serde::{Deserialize, Serialize};
2025-07-21 02:11:23 -04:00
use serde_valid::Validate;
use std::fmt::Display;
2025-07-20 02:49:42 -04:00
#[derive(Serialize, Deserialize)]
pub struct Entry {
pub slug: String,
pub edit_code: String,
pub salt: String,
pub created: usize,
pub edited: usize,
pub content: String,
2025-07-21 02:11:23 -04:00
pub metadata: String,
}
#[derive(Serialize, Deserialize, PartialEq, Eq)]
pub enum RecommendedTheme {
#[serde(alias = "none")]
None,
#[serde(alias = "light")]
Light,
#[serde(alias = "dark")]
Dark,
}
impl Default for RecommendedTheme {
fn default() -> Self {
Self::None
}
}
#[derive(Serialize, Deserialize, PartialEq, Eq)]
pub enum TextAlignment {
#[serde(alias = "left")]
Left,
#[serde(alias = "center")]
Center,
#[serde(alias = "right")]
Right,
#[serde(alias = "justify")]
Justify,
}
impl Default for TextAlignment {
fn default() -> Self {
Self::Left
}
}
impl Display for TextAlignment {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Left => "left",
Self::Center => "center",
Self::Right => "right",
Self::Justify => "justify",
})
}
}
#[derive(Serialize, Deserialize, Validate, Default)]
pub struct EntryMetadata {
/// The title of the page.
#[serde(default, alias = "PAGE_TITLE")]
#[validate(max_length = 128)]
pub page_title: String,
/// The description of the page.
#[serde(default, alias = "PAGE_DESCRIPTION")]
#[validate(max_length = 256)]
pub page_description: String,
/// The favicon of the page.
#[serde(default, alias = "PAGE_icon")]
#[validate(max_length = 128)]
pub page_icon: String,
/// The title of the page shown in external embeds.
#[serde(default, alias = "SHARE_TITLE")]
#[validate(max_length = 128)]
pub share_title: String,
/// The description of the page shown in external embeds.
#[serde(default, alias = "SHARE_DESCRIPTION")]
#[validate(max_length = 256)]
pub share_description: String,
/// The image shown in external embeds.
#[serde(default, alias = "SHARE_IMAGE")]
#[validate(max_length = 128)]
pub share_image: String,
/// If views are counted and shown for this entry.
#[serde(default, alias = "OPTION_DISABLE_VIEWS")]
pub option_disable_views: bool,
/// If this entry shows up in search engines.
#[serde(default, alias = "OPTION_DISABLE_SEARCH_ENGINE")]
pub option_disable_search_engine: bool,
/// The theme that is automatically used when this entry is viewed.
#[serde(default, alias = "ACCESS_RECOMMENDED_THEME")]
pub access_recommended_theme: RecommendedTheme,
/// The slug of the easy-to-read (no metadata) version of this entry.
///
/// Should not begin with "/".
#[serde(default, alias = "ACCESS_EASY_READ")]
#[validate(max_length = 32)]
pub access_easy_read: String,
/// The color of the text in the inner container.
#[serde(default, alias = "CONTAINER_INNER_FOREGROUND")]
#[validate(max_length = 32)]
pub container_inner_foreground: String,
/// The background of the inner container.
///
/// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/background>
#[serde(default, alias = "CONTAINER_INNER_BACKGROUND")]
#[validate(max_length = 256)]
pub container_inner_background: String,
/// The color of the text in the outer container.
#[serde(default, alias = "CONTAINER_OUTER_FOREGROUND")]
#[validate(max_length = 32)]
pub container_outer_foreground: String,
/// The background of the outer container.
///
/// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/background>
#[serde(default, alias = "CONTAINER_OUTER_BACKGROUND")]
#[validate(max_length = 256)]
pub container_outer_background: String,
/// The border around the container.
///
/// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/border>
#[serde(default, alias = "CONTAINER_BORDER")]
#[validate(max_length = 256)]
pub container_border: String,
/// The shadow around the container.
///
/// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/box-shadow>
#[serde(default, alias = "CONTAINER_SHADOW")]
#[validate(max_length = 32)]
pub container_shadow: String,
/// The name of a font from Google Fonts to use.
#[serde(default, alias = "CONTENT_FONT")]
#[validate(max_length = 32)]
pub content_font: String,
/// The weight to use for the body text.
#[serde(default, alias = "CONTENT_FONT_WEIGHT")]
pub content_font_weight: u32,
/// The text size of elements by element tag.
///
/// # Example
/// ```toml
/// # ...
/// content_text_size = [["h1", "16px"]]
/// ```
#[serde(default, alias = "CONTENT_TEXT_SIZE")]
pub content_text_size: Vec<(String, String)>,
/// The default text alignment.
#[serde(default, alias = "CONTENT_TEXT_ALIGN")]
pub content_text_align: TextAlignment,
/// The color of links.
#[serde(default, alias = "CONTENT_TEXT_LINK_COLOR")]
pub content_text_link_color: String,
}
macro_rules! metadata_css {
($selector:literal, $property:literal, $self:ident.$field:ident->$output:ident) => {
if !$self.$field.is_empty() {
$output.push_str(&format!(
"{} {{ {}: {}; }}\n",
$selector,
$property,
EntryMetadata::css_escape(&$self.$field)
));
}
};
}
impl EntryMetadata {
pub fn head_tags(&self) -> String {
let mut output = String::new();
if !self.page_title.is_empty() {
output.push_str(&format!("<title>{}</title>", self.page_title));
}
if !self.page_description.is_empty() {
output.push_str(&format!(
"<meta name=\"description\" content=\"{}\" />",
self.page_description.replace("\"", "\\\"")
));
}
if !self.page_icon.is_empty() {
output.push_str(&format!(
"<link rel=\"icon\" href=\"{}\" />",
self.page_icon.replace("\"", "\\\"")
));
}
if !self.share_title.is_empty() {
output.push_str(&format!(
"<meta property=\"og:title\" content=\"{}\" /><meta property=\"twitter:title\" content=\"{}\" />",
self.share_title.replace("\"", "\\\""),
self.share_title.replace("\"", "\\\"")
));
}
if !self.share_description.is_empty() {
output.push_str(&format!(
"<meta property=\"og:description\" content=\"{}\" /><meta property=\"twitter:description\" content=\"{}\" />",
self.share_description.replace("\"", "\\\""),
self.share_description.replace("\"", "\\\"")
));
}
if !self.share_image.is_empty() {
output.push_str(&format!(
"<meta property=\"og:image\" content=\"{}\" /><meta property=\"twitter:image\" content=\"{}\" />",
self.share_image.replace("\"", "\\\""),
self.share_image.replace("\"", "\\\"")
));
}
if !self.content_font.is_empty() {
output.push_str(&format!(
"<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">
<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>
<link href=\"https://fonts.googleapis.com/css2?family={}:wght@100..900&display=swap\" rel=\"stylesheet\">",
self.content_font.replace(" ", "+"),
));
}
output
}
pub fn css_escape(input: &str) -> String {
input.replace("}", "").replace(";", "").replace("/*", "")
}
pub fn css(&self) -> String {
let mut output = "<style>".to_string();
metadata_css!(".container", "color", self.container_inner_foreground->output);
metadata_css!(".container", "background", self.container_inner_background->output);
metadata_css!("body", "color", self.container_outer_foreground->output);
metadata_css!("body", "background", self.container_outer_background->output);
metadata_css!(".container", "border", self.container_border->output);
metadata_css!(".container", "box-shadow", self.container_shadow->output);
metadata_css!(".container a", "color", self.content_text_link_color->output);
if self.content_text_align != TextAlignment::Left {
output.push_str(&format!(
".container {{ text-align: {}; }}\n",
EntryMetadata::css_escape(&self.content_text_align.to_string())
));
}
for (element, size) in &self.content_text_size {
output.push_str(&format!(
".container {} {{ font-size: {}; }}\n",
element,
EntryMetadata::css_escape(&size)
));
}
if !self.content_font.is_empty() {
output.push_str(&format!(
".container {{ font-family: \"{}\", system-ui; }}",
self.content_font
));
}
if self.content_font_weight != 0 {
output.push_str(&format!(
".container {{ font-weight: {}; }}",
self.content_font_weight
));
}
output + "</style>"
}
2025-07-20 02:49:42 -04:00
}