add: entry metadata

This commit is contained in:
trisua 2025-07-21 02:11:23 -04:00
parent d80368e6c2
commit b505199492
11 changed files with 631 additions and 45 deletions

186
Cargo.lock generated
View file

@ -84,7 +84,7 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "attobin"
version = "0.1.1"
version = "0.2.0"
dependencies = [
"axum",
"axum-extra",
@ -94,10 +94,12 @@ dependencies = [
"pathbufd",
"serde",
"serde_json",
"serde_valid",
"tera",
"tetratto-core",
"tetratto-shared",
"tokio",
"toml 0.9.2",
"tower-http",
"tracing",
"tracing-subscriber",
@ -555,6 +557,12 @@ dependencies = [
"dtoa",
]
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "emojis"
version = "0.7.0"
@ -1163,6 +1171,7 @@ checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661"
dependencies = [
"equivalent",
"hashbrown",
"serde",
]
[[package]]
@ -1192,6 +1201,15 @@ dependencies = [
"serde",
]
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.15"
@ -1579,7 +1597,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323"
dependencies = [
"memchr",
"thiserror",
"thiserror 2.0.12",
"ucd-trie",
]
@ -1776,6 +1794,27 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]]
name = "proc-macro-error-attr2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
dependencies = [
"proc-macro2",
"quote",
]
[[package]]
name = "proc-macro-error2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
dependencies = [
"proc-macro-error-attr2",
"proc-macro2",
"quote",
]
[[package]]
name = "proc-macro2"
version = "1.0.95"
@ -2185,6 +2224,15 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_spanned"
version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
dependencies = [
"serde",
]
[[package]]
name = "serde_spanned"
version = "1.0.0"
@ -2206,6 +2254,52 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_valid"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b615bed66931a7a9809b273937adc8a402d038b1e509d027fcaf62f084d33d1"
dependencies = [
"indexmap",
"itertools",
"num-traits",
"once_cell",
"paste",
"regex",
"serde",
"serde_json",
"serde_valid_derive",
"serde_valid_literal",
"thiserror 1.0.69",
"toml 0.8.23",
"unicode-segmentation",
]
[[package]]
name = "serde_valid_derive"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fa1a5a21ea5aab06d2e6a6b59837d450fb2be9695be97735a711edfbe79ea07"
dependencies = [
"itertools",
"paste",
"proc-macro-error2",
"proc-macro2",
"quote",
"strsim",
"syn",
]
[[package]]
name = "serde_valid_literal"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd07331596ea967dccf9a35bde71ecd757490e09827b938a5c6226c648e3a25e"
dependencies = [
"paste",
"regex",
]
[[package]]
name = "sha1"
version = "0.10.6"
@ -2344,6 +2438,12 @@ dependencies = [
"unicode-properties",
]
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "subtle"
version = "2.6.1"
@ -2469,7 +2569,7 @@ dependencies = [
"serde_json",
"tetratto-l10n",
"tetratto-shared",
"toml",
"toml 0.9.2",
"totp-rs",
]
@ -2481,7 +2581,7 @@ checksum = "d96f5e41633c757e3519efb47c9b85d00d14322c1961360e126d0ecc0ea79b86"
dependencies = [
"pathbufd",
"serde",
"toml",
"toml 0.9.2",
]
[[package]]
@ -2502,13 +2602,33 @@ dependencies = [
"uuid",
]
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl 1.0.69",
]
[[package]]
name = "thiserror"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
dependencies = [
"thiserror-impl",
"thiserror-impl 2.0.12",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
@ -2688,6 +2808,18 @@ dependencies = [
"tokio",
]
[[package]]
name = "toml"
version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
dependencies = [
"serde",
"serde_spanned 0.6.9",
"toml_datetime 0.6.11",
"toml_edit",
]
[[package]]
name = "toml"
version = "0.9.2"
@ -2696,13 +2828,22 @@ checksum = "ed0aee96c12fa71097902e0bb061a5e1ebd766a6636bb605ba401c45c1650eac"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"serde_spanned 1.0.0",
"toml_datetime 0.7.0",
"toml_parser",
"toml_writer",
"winnow",
]
[[package]]
name = "toml_datetime"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
dependencies = [
"serde",
]
[[package]]
name = "toml_datetime"
version = "0.7.0"
@ -2712,6 +2853,20 @@ dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.22.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [
"indexmap",
"serde",
"serde_spanned 0.6.9",
"toml_datetime 0.6.11",
"toml_write",
"winnow",
]
[[package]]
name = "toml_parser"
version = "1.0.1"
@ -2721,6 +2876,12 @@ dependencies = [
"winnow",
]
[[package]]
name = "toml_write"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]]
name = "toml_writer"
version = "1.0.2"
@ -2881,7 +3042,7 @@ dependencies = [
"log",
"rand 0.9.1",
"sha1",
"thiserror",
"thiserror 2.0.12",
"utf-8",
]
@ -2980,6 +3141,12 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0"
[[package]]
name = "unicode-segmentation"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "unicode-width"
version = "0.2.1"
@ -3448,6 +3615,9 @@ name = "winnow"
version = "0.7.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95"
dependencies = [
"memchr",
]
[[package]]
name = "wit-bindgen-rt"

View file

@ -1,6 +1,6 @@
[package]
name = "attobin"
version = "0.1.1"
version = "0.2.0"
edition = "2024"
authors = ["trisuaso"]
repository = "https://trisua.com/t/tetratto"
@ -28,3 +28,5 @@ nanoneo = "0.2.0"
dotenv = "0.15.0"
glob = "0.3.2"
serde_json = "1.0.141"
toml = "0.9.2"
serde_valid = { version = "1.0.5", features = ["toml"] }

View file

@ -35,6 +35,18 @@ function media_theme_pref() {
}
}
globalThis.temporary_set_theme = (theme) => {
document.documentElement.className = theme.toLowerCase();
if (theme === "Light") {
document.getElementById("switch_light").classList.remove("hidden");
document.getElementById("switch_dark").classList.add("hidden");
} else {
document.getElementById("switch_light").classList.add("hidden");
document.getElementById("switch_dark").classList.remove("hidden");
}
};
globalThis.set_theme = (theme) => {
window.localStorage.setItem("attobin:theme", theme);
document.documentElement.className = theme;
@ -67,14 +79,26 @@ function check_message() {
}
}
globalThis.show_message = (message, message_good = true) => {
const element = document.getElementById("messages");
element.style.marginBottom = "1rem";
element.style.paddingLeft = "1rem";
element.innerHTML += `<li class="${message_good ? "green" : "red"}">${message.replaceAll('"', "")}</li>`;
};
check_message();
// editor
globalThis.init_editor = () => {
globalThis.editor = CodeMirror(document.getElementById("editor_tab"), {
value: (document.getElementById("editor_content") || { innerHTML: "" })
globalThis.init_editor = (
name = "editor",
mode = "markdown",
element = "editor_tab",
content_element = "editor_content",
) => {
globalThis[name] = CodeMirror(document.getElementById(element), {
value: (document.getElementById(content_element) || { innerHTML: "" })
.innerHTML,
mode: "markdown",
mode,
lineWrapping: true,
lineNumbers: false,
autoCloseBrackets: true,
@ -99,20 +123,24 @@ globalThis.init_editor = () => {
},
});
if (name === "editor") {
window.addEventListener("beforeunload", (e) => {
if (!globalThis.ALLOW_LEAVE) {
e.preventDefault();
return null;
}
});
}
};
globalThis.tab_editor = () => {
document.getElementById("editor_tab").classList.remove("hidden");
document.getElementById("preview_tab").classList.add("hidden");
document.getElementById("metadata_tab").classList.add("hidden");
document.getElementById("editor_tab_button").classList.remove("camo");
document.getElementById("preview_tab_button").classList.add("camo");
document.getElementById("metadata_tab_button").classList.add("camo");
};
globalThis.tab_preview = async () => {
@ -133,9 +161,28 @@ globalThis.tab_preview = async () => {
// ...
document.getElementById("editor_tab").classList.add("hidden");
document.getElementById("preview_tab").classList.remove("hidden");
document.getElementById("metadata_tab").classList.add("hidden");
document.getElementById("editor_tab_button").classList.add("camo");
document.getElementById("preview_tab_button").classList.remove("camo");
document.getElementById("metadata_tab_button").classList.add("camo");
};
globalThis.first_time_on_metadata_tab = true;
globalThis.tab_metadata = () => {
document.getElementById("editor_tab").classList.add("hidden");
document.getElementById("preview_tab").classList.add("hidden");
document.getElementById("metadata_tab").classList.remove("hidden");
document.getElementById("editor_tab_button").classList.add("camo");
document.getElementById("preview_tab_button").classList.add("camo");
document.getElementById("metadata_tab_button").classList.remove("camo");
if (globalThis.first_time_on_metadata_tab) {
globalThis.metadata_editor.refresh();
}
globalThis.first_time_on_metadata_tab = false;
};
let exists_timeout = null;

View file

@ -178,6 +178,10 @@ video {
appearance: none;
}
.button.small {
--h: 28px;
}
.button:hover {
background: var(--color-super-raised);
}

View file

@ -1,6 +1,7 @@
(text "{% extends \"root.lisp\" %} {% block head %}")
(title
(text "{{ entry.slug }}"))
(link ("rel" "icon") ("href" "/public/favicon.svg"))
(text "{% endblock %} {% block body %}")
(div
("class" "flex items-center bar")
@ -13,7 +14,12 @@
("class" "button camo tab_button")
("id" "preview_tab_button")
("onclick" "tab_preview()")
(text "Preview")))
(text "Preview"))
(button
("class" "button camo tab_button")
("id" "metadata_tab_button")
("onclick" "tab_metadata()")
(text "Metadata")))
(div
("class" "card tab tabs")
(div
@ -21,6 +27,9 @@
("class" "tab fadein"))
(div
("id" "preview_tab")
("class" "tab fadein hidden container"))
(div
("id" "metadata_tab")
("class" "tab fadein hidden")))
(form
("class" "w-full flex flex-col gap-2")
@ -75,10 +84,12 @@
(link ("rel" "stylesheet") ("href" "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/default.min.css"))
(script ("id" "editor_content") ("type" "text/markdown") (text "{{ entry.content|remove_script_tags|safe }}"))
(script ("id" "editor_metadata_content") ("type" "text/markdown") (text "{{ entry.metadata|remove_script_tags|safe }}"))
(script
(text "setTimeout(() => {
globalThis.init_editor();
globalThis.init_editor(\"metadata_editor\", \"plain\", \"metadata_tab\", \"editor_metadata_content\");
}, 150);
globalThis.edit_entry = (e) => {
@ -99,6 +110,7 @@
edit_code: e.target.edit_code.value,
new_slug: e.target.new_slug.value || undefined,
new_edit_code: e.target.new_edit_code.value || undefined,
metadata: globalThis.metadata_editor.getValue(),
\"delete\": rm,
}),
})
@ -117,8 +129,7 @@
window.location.href = \"/\";
}
} else {
document.cookie = `Atto-Message=\"${res.message}\"; path=/`;
check_message();
show_message(res.message, false);
}
})
}"))

View file

@ -1,6 +1,7 @@
(text "{% extends \"root.lisp\" %} {% block head %}")
(title
(text "Error - {{ name }}"))
(link ("rel" "icon") ("href" "/public/favicon.svg"))
(text "{% endblock %} {% block body %}")
(div
("class" "card")

View file

@ -1,6 +1,7 @@
(text "{% extends \"root.lisp\" %} {% block head %}")
(title
(text "{{ name }}"))
(link ("rel" "icon") ("href" "/public/favicon.svg"))
(text "{% endblock %} {% block body %}")
(div
("class" "flex items-center bar")
@ -13,7 +14,12 @@
("class" "button camo tab_button")
("id" "preview_tab_button")
("onclick" "tab_preview()")
(text "Preview")))
(text "Preview"))
(button
("class" "button camo tab_button")
("id" "metadata_tab_button")
("onclick" "tab_metadata()")
(text "Metadata")))
(div
("class" "card tab tabs")
(div
@ -21,6 +27,9 @@
("class" "tab fadein"))
(div
("id" "preview_tab")
("class" "tab fadein hidden container"))
(div
("id" "metadata_tab")
("class" "tab fadein hidden")))
(form
("class" "w-full flex justify-between gap-2 flex-collapse-rev")
@ -58,6 +67,7 @@
(script
(text "setTimeout(() => {
globalThis.init_editor();
globalThis.init_editor(\"metadata_editor\", \"plain\", \"metadata_tab\");
}, 150);
globalThis.create_entry = (e) => {
@ -71,6 +81,7 @@
content: globalThis.editor.getValue(),
slug: e.target.slug.value || undefined,
edit_code: e.target.edit_code.value || undefined,
metadata: globalThis.metadata_editor.getValue(),
}),
})
.then(res => res.json())
@ -81,8 +92,7 @@
document.cookie = \"Atto-Message-Good=true; path=/\";
window.location.href = `/${res.payload[0]}`;
} else {
document.cookie = `Atto-Message=\"${res.message}\"; path=/`;
check_message();
show_message(res.message, false);
}
})
}"))

View file

@ -7,12 +7,10 @@
(meta ("name" "viewport") ("content" "width=device-width, initial-scale=1.0"))
(meta ("http-equiv" "X-UA-Compatible") ("content" "ie=edge"))
(link ("rel" "icon") ("href" "/public/favicon.svg"))
(link ("rel" "stylesheet") ("href" "{{ tetratto }}/css/utility.css?v={{ build_code }}"))
(link ("rel" "stylesheet") ("href" "/public/style.css?v={{ build_code }}"))
(meta ("name" "theme-color") ("content" "#fbc27f"))
(meta ("name" "description") ("content" "{{ name }}"))
(meta ("property" "og:type") ("content" "website"))
(meta ("property" "og:site_name") ("content" "{{ name }}"))

View file

@ -1,11 +1,14 @@
(text "{% extends \"root.lisp\" %} {% block head %}")
(text "{% if not metadata.page_title -%}")
(title
(text "{{ entry.slug }}"))
(text "{%- endif %} {{ metadata_head|safe }}")
(link ("rel" "icon") ("href" "/public/favicon.svg"))
(text "{% endblock %} {% block body %}")
(div
("class" "flex flex-col gap-2")
(div
("class" "card")
("class" "card container")
("style" "min-height: 15rem")
(text "{{ entry.content|markdown|safe }}"))
(div
@ -17,9 +20,27 @@
(div
("class" "flex flex-col gap-1 items-end fade")
; dates
(span (text "Pub: {{ entry.created / 1000|int|date(format=\"%Y-%m-%d %H:%M\", timezone=\"Etc/UTC\") }} UTC"))
(span (text "Edit: {{ entry.edited / 1000|int|date(format=\"%Y-%m-%d %H:%M\", timezone=\"Etc/UTC\") }} UTC"))
(span (text "Views: {{ views }}")))))
; auto theme
(text "{% if metadata.access_recommended_theme != 'None' -%}")
(span (text "Auto theme: {{ metadata.access_recommended_theme }}"))
(script ("defer" "true") (text "setTimeout(() => { temporary_set_theme('{{ metadata.access_recommended_theme }}') }, 150);"))
(text "{%- endif %}")
; views
(text "{% if not metadata.option_disable_views -%}")
(span (text "Views: {{ views }}"))
(text "{%- endif %}")
; easy-to-read
(text "{% if metadata.access_easy_read|length > 0 -%}")
(a ("class" "button small") ("href" "/{{ metadata.access_easy_read }}") (b (text "E2R")))
(text "{%- endif %}"))))
(text "{{ metadata_css|safe }}")
(link ("rel" "stylesheet") ("href" "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/default.min.css"))
(script ("src" "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"))

View file

@ -1,4 +1,6 @@
use serde::{Deserialize, Serialize};
use serde_valid::Validate;
use std::fmt::Display;
#[derive(Serialize, Deserialize)]
pub struct Entry {
@ -8,4 +10,266 @@ pub struct Entry {
pub created: usize,
pub edited: usize,
pub content: String,
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>"
}
}

View file

@ -1,4 +1,7 @@
use crate::{State, model::Entry};
use crate::{
State,
model::{Entry, EntryMetadata},
};
use axum::{
Extension, Json, Router,
extract::Path,
@ -6,6 +9,7 @@ use axum::{
routing::{get, get_service, post},
};
use serde::Deserialize;
use serde_valid::Validate;
use tera::Context;
use tetratto_core::{
model::{
@ -99,7 +103,25 @@ async fn view_request(
}
};
let (views_id, views) = match data
// check metadata
let metadata: EntryMetadata = match toml::from_str(&entry.metadata) {
Ok(x) => x,
Err(e) => {
let mut ctx = default_context(&data, &build_code);
ctx.insert("error", &e.to_string());
return Html(tera.render("error.lisp", &ctx).unwrap());
}
};
if let Err(e) = metadata.validate() {
let mut ctx = default_context(&data, &build_code);
ctx.insert("error", &e.to_string());
return Html(tera.render("error.lisp", &ctx).unwrap());
}
// pull views
let views = if !metadata.option_disable_views {
match data
.query(&SimplifiedQuery {
query: AppDataSelectQuery::KeyIs(format!("entries.views('{}')", slug)),
mode: AppDataSelectMode::One(0),
@ -107,7 +129,19 @@ async fn view_request(
.await
{
Ok(r) => match r {
AppDataQueryResult::One(r) => (r.id, r.value.parse::<usize>().unwrap()),
AppDataQueryResult::One(r) => {
// count view
let views = r.value.parse::<usize>().unwrap();
if let Err(e) = data.update(r.id, (views + 1).to_string()).await {
let mut ctx = default_context(&data, &build_code);
ctx.insert("error", &e.to_string());
return Html(tera.render("error.lisp", &ctx).unwrap());
}
views
}
AppDataQueryResult::Many(_) => unreachable!(),
},
Err(e) => {
@ -116,21 +150,19 @@ async fn view_request(
return Html(tera.render("error.lisp", &ctx).unwrap());
}
};
// count view
if let Err(e) = data.update(views_id, (views + 1).to_string()).await {
let mut ctx = default_context(&data, &build_code);
ctx.insert("error", &e.to_string());
return Html(tera.render("error.lisp", &ctx).unwrap());
}
} else {
0
};
// ...
let mut ctx = default_context(&data, &build_code);
ctx.insert("entry", &entry);
ctx.insert("views", &views);
ctx.insert("metadata", &metadata);
ctx.insert("metadata_head", &metadata.head_tags());
ctx.insert("metadata_css", &metadata.css());
Html(tera.render("view.lisp", &ctx).unwrap())
}
@ -201,6 +233,8 @@ async fn exists_request(
#[derive(Deserialize)]
struct CreateEntry {
content: String,
#[serde(default)]
metadata: String,
#[serde(default = "default_random")]
slug: String,
#[serde(default = "default_random")]
@ -234,6 +268,16 @@ async fn create_request(
return Json(Error::DataTooLong("content".to_string()).into());
}
// check metadata
let metadata: EntryMetadata = match toml::from_str(&req.metadata) {
Ok(x) => x,
Err(e) => return Json(Error::MiscError(e.to_string()).into()),
};
if let Err(e) = metadata.validate() {
return Json(Error::MiscError(e.to_string()).into());
}
// check for existing
if data
.query(&SimplifiedQuery {
@ -260,6 +304,7 @@ async fn create_request(
created,
edited: created,
content: req.content,
metadata: req.metadata,
})
.unwrap(),
)
@ -292,6 +337,8 @@ struct EditEntry {
#[serde(default)]
new_edit_code: Option<String>,
#[serde(default)]
metadata: String,
#[serde(default)]
delete: bool,
}
@ -311,6 +358,16 @@ async fn edit_request(
return Json(Error::DataTooLong("content".to_string()).into());
}
// check metadata
let metadata: EntryMetadata = match toml::from_str(&req.metadata) {
Ok(x) => x,
Err(e) => return Json(Error::MiscError(e.to_string()).into()),
};
if let Err(e) = metadata.validate() {
return Json(Error::MiscError(e.to_string()).into());
}
// ...
let (id, mut entry) = match data
.query(&SimplifiedQuery {
@ -418,6 +475,7 @@ async fn edit_request(
// update
entry.content = req.content;
entry.metadata = req.metadata;
entry.edited = unix_epoch_timestamp();
if let Err(e) = data