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]] [[package]]
name = "attobin" name = "attobin"
version = "0.1.1" version = "0.2.0"
dependencies = [ dependencies = [
"axum", "axum",
"axum-extra", "axum-extra",
@ -94,10 +94,12 @@ dependencies = [
"pathbufd", "pathbufd",
"serde", "serde",
"serde_json", "serde_json",
"serde_valid",
"tera", "tera",
"tetratto-core", "tetratto-core",
"tetratto-shared", "tetratto-shared",
"tokio", "tokio",
"toml 0.9.2",
"tower-http", "tower-http",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
@ -555,6 +557,12 @@ dependencies = [
"dtoa", "dtoa",
] ]
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]] [[package]]
name = "emojis" name = "emojis"
version = "0.7.0" version = "0.7.0"
@ -1163,6 +1171,7 @@ checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown", "hashbrown",
"serde",
] ]
[[package]] [[package]]
@ -1192,6 +1201,15 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.15" version = "1.0.15"
@ -1579,7 +1597,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323"
dependencies = [ dependencies = [
"memchr", "memchr",
"thiserror", "thiserror 2.0.12",
"ucd-trie", "ucd-trie",
] ]
@ -1776,6 +1794,27 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" 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]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.95" version = "1.0.95"
@ -2185,6 +2224,15 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "serde_spanned"
version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "serde_spanned" name = "serde_spanned"
version = "1.0.0" version = "1.0.0"
@ -2206,6 +2254,52 @@ dependencies = [
"serde", "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]] [[package]]
name = "sha1" name = "sha1"
version = "0.10.6" version = "0.10.6"
@ -2344,6 +2438,12 @@ dependencies = [
"unicode-properties", "unicode-properties",
] ]
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]] [[package]]
name = "subtle" name = "subtle"
version = "2.6.1" version = "2.6.1"
@ -2469,7 +2569,7 @@ dependencies = [
"serde_json", "serde_json",
"tetratto-l10n", "tetratto-l10n",
"tetratto-shared", "tetratto-shared",
"toml", "toml 0.9.2",
"totp-rs", "totp-rs",
] ]
@ -2481,7 +2581,7 @@ checksum = "d96f5e41633c757e3519efb47c9b85d00d14322c1961360e126d0ecc0ea79b86"
dependencies = [ dependencies = [
"pathbufd", "pathbufd",
"serde", "serde",
"toml", "toml 0.9.2",
] ]
[[package]] [[package]]
@ -2502,13 +2602,33 @@ dependencies = [
"uuid", "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]] [[package]]
name = "thiserror" name = "thiserror"
version = "2.0.12" version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
dependencies = [ 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]] [[package]]
@ -2688,6 +2808,18 @@ dependencies = [
"tokio", "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]] [[package]]
name = "toml" name = "toml"
version = "0.9.2" version = "0.9.2"
@ -2696,13 +2828,22 @@ checksum = "ed0aee96c12fa71097902e0bb061a5e1ebd766a6636bb605ba401c45c1650eac"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"serde", "serde",
"serde_spanned", "serde_spanned 1.0.0",
"toml_datetime", "toml_datetime 0.7.0",
"toml_parser", "toml_parser",
"toml_writer", "toml_writer",
"winnow", "winnow",
] ]
[[package]]
name = "toml_datetime"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "toml_datetime" name = "toml_datetime"
version = "0.7.0" version = "0.7.0"
@ -2712,6 +2853,20 @@ dependencies = [
"serde", "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]] [[package]]
name = "toml_parser" name = "toml_parser"
version = "1.0.1" version = "1.0.1"
@ -2721,6 +2876,12 @@ dependencies = [
"winnow", "winnow",
] ]
[[package]]
name = "toml_write"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]] [[package]]
name = "toml_writer" name = "toml_writer"
version = "1.0.2" version = "1.0.2"
@ -2881,7 +3042,7 @@ dependencies = [
"log", "log",
"rand 0.9.1", "rand 0.9.1",
"sha1", "sha1",
"thiserror", "thiserror 2.0.12",
"utf-8", "utf-8",
] ]
@ -2980,6 +3141,12 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0"
[[package]]
name = "unicode-segmentation"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]] [[package]]
name = "unicode-width" name = "unicode-width"
version = "0.2.1" version = "0.2.1"
@ -3448,6 +3615,9 @@ name = "winnow"
version = "0.7.12" version = "0.7.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "wit-bindgen-rt" name = "wit-bindgen-rt"

View file

@ -1,6 +1,6 @@
[package] [package]
name = "attobin" name = "attobin"
version = "0.1.1" version = "0.2.0"
edition = "2024" edition = "2024"
authors = ["trisuaso"] authors = ["trisuaso"]
repository = "https://trisua.com/t/tetratto" repository = "https://trisua.com/t/tetratto"
@ -28,3 +28,5 @@ nanoneo = "0.2.0"
dotenv = "0.15.0" dotenv = "0.15.0"
glob = "0.3.2" glob = "0.3.2"
serde_json = "1.0.141" 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) => { globalThis.set_theme = (theme) => {
window.localStorage.setItem("attobin:theme", theme); window.localStorage.setItem("attobin:theme", theme);
document.documentElement.className = 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(); check_message();
// editor // editor
globalThis.init_editor = () => { globalThis.init_editor = (
globalThis.editor = CodeMirror(document.getElementById("editor_tab"), { name = "editor",
value: (document.getElementById("editor_content") || { innerHTML: "" }) mode = "markdown",
element = "editor_tab",
content_element = "editor_content",
) => {
globalThis[name] = CodeMirror(document.getElementById(element), {
value: (document.getElementById(content_element) || { innerHTML: "" })
.innerHTML, .innerHTML,
mode: "markdown", mode,
lineWrapping: true, lineWrapping: true,
lineNumbers: false, lineNumbers: false,
autoCloseBrackets: true, autoCloseBrackets: true,
@ -99,20 +123,24 @@ globalThis.init_editor = () => {
}, },
}); });
window.addEventListener("beforeunload", (e) => { if (name === "editor") {
if (!globalThis.ALLOW_LEAVE) { window.addEventListener("beforeunload", (e) => {
e.preventDefault(); if (!globalThis.ALLOW_LEAVE) {
return null; e.preventDefault();
} return null;
}); }
});
}
}; };
globalThis.tab_editor = () => { globalThis.tab_editor = () => {
document.getElementById("editor_tab").classList.remove("hidden"); document.getElementById("editor_tab").classList.remove("hidden");
document.getElementById("preview_tab").classList.add("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("editor_tab_button").classList.remove("camo");
document.getElementById("preview_tab_button").classList.add("camo"); document.getElementById("preview_tab_button").classList.add("camo");
document.getElementById("metadata_tab_button").classList.add("camo");
}; };
globalThis.tab_preview = async () => { globalThis.tab_preview = async () => {
@ -133,9 +161,28 @@ globalThis.tab_preview = async () => {
// ... // ...
document.getElementById("editor_tab").classList.add("hidden"); document.getElementById("editor_tab").classList.add("hidden");
document.getElementById("preview_tab").classList.remove("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("editor_tab_button").classList.add("camo");
document.getElementById("preview_tab_button").classList.remove("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; let exists_timeout = null;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,11 +1,14 @@
(text "{% extends \"root.lisp\" %} {% block head %}") (text "{% extends \"root.lisp\" %} {% block head %}")
(text "{% if not metadata.page_title -%}")
(title (title
(text "{{ entry.slug }}")) (text "{{ entry.slug }}"))
(text "{%- endif %} {{ metadata_head|safe }}")
(link ("rel" "icon") ("href" "/public/favicon.svg"))
(text "{% endblock %} {% block body %}") (text "{% endblock %} {% block body %}")
(div (div
("class" "flex flex-col gap-2") ("class" "flex flex-col gap-2")
(div (div
("class" "card") ("class" "card container")
("style" "min-height: 15rem") ("style" "min-height: 15rem")
(text "{{ entry.content|markdown|safe }}")) (text "{{ entry.content|markdown|safe }}"))
(div (div
@ -17,9 +20,27 @@
(div (div
("class" "flex flex-col gap-1 items-end fade") ("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 "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 "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")) (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")) (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::{Deserialize, Serialize};
use serde_valid::Validate;
use std::fmt::Display;
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct Entry { pub struct Entry {
@ -8,4 +10,266 @@ pub struct Entry {
pub created: usize, pub created: usize,
pub edited: usize, pub edited: usize,
pub content: String, 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::{ use axum::{
Extension, Json, Router, Extension, Json, Router,
extract::Path, extract::Path,
@ -6,6 +9,7 @@ use axum::{
routing::{get, get_service, post}, routing::{get, get_service, post},
}; };
use serde::Deserialize; use serde::Deserialize;
use serde_valid::Validate;
use tera::Context; use tera::Context;
use tetratto_core::{ use tetratto_core::{
model::{ model::{
@ -99,38 +103,66 @@ async fn view_request(
} }
}; };
let (views_id, views) = match data // check metadata
.query(&SimplifiedQuery { let metadata: EntryMetadata = match toml::from_str(&entry.metadata) {
query: AppDataSelectQuery::KeyIs(format!("entries.views('{}')", slug)), Ok(x) => x,
mode: AppDataSelectMode::One(0),
})
.await
{
Ok(r) => match r {
AppDataQueryResult::One(r) => (r.id, r.value.parse::<usize>().unwrap()),
AppDataQueryResult::Many(_) => unreachable!(),
},
Err(e) => { Err(e) => {
let mut ctx = default_context(&data, &build_code); let mut ctx = default_context(&data, &build_code);
ctx.insert("error", &e.to_string()); ctx.insert("error", &e.to_string());
return Html(tera.render("error.lisp", &ctx).unwrap()); return Html(tera.render("error.lisp", &ctx).unwrap());
} }
}; };
// count view if let Err(e) = metadata.validate() {
if let Err(e) = data.update(views_id, (views + 1).to_string()).await {
let mut ctx = default_context(&data, &build_code); let mut ctx = default_context(&data, &build_code);
ctx.insert("error", &e.to_string()); ctx.insert("error", &e.to_string());
return Html(tera.render("error.lisp", &ctx).unwrap()); 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),
})
.await
{
Ok(r) => match r {
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) => {
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); let mut ctx = default_context(&data, &build_code);
ctx.insert("entry", &entry); ctx.insert("entry", &entry);
ctx.insert("views", &views); 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()) Html(tera.render("view.lisp", &ctx).unwrap())
} }
@ -201,6 +233,8 @@ async fn exists_request(
#[derive(Deserialize)] #[derive(Deserialize)]
struct CreateEntry { struct CreateEntry {
content: String, content: String,
#[serde(default)]
metadata: String,
#[serde(default = "default_random")] #[serde(default = "default_random")]
slug: String, slug: String,
#[serde(default = "default_random")] #[serde(default = "default_random")]
@ -234,6 +268,16 @@ async fn create_request(
return Json(Error::DataTooLong("content".to_string()).into()); 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 // check for existing
if data if data
.query(&SimplifiedQuery { .query(&SimplifiedQuery {
@ -260,6 +304,7 @@ async fn create_request(
created, created,
edited: created, edited: created,
content: req.content, content: req.content,
metadata: req.metadata,
}) })
.unwrap(), .unwrap(),
) )
@ -292,6 +337,8 @@ struct EditEntry {
#[serde(default)] #[serde(default)]
new_edit_code: Option<String>, new_edit_code: Option<String>,
#[serde(default)] #[serde(default)]
metadata: String,
#[serde(default)]
delete: bool, delete: bool,
} }
@ -311,6 +358,16 @@ async fn edit_request(
return Json(Error::DataTooLong("content".to_string()).into()); 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 let (id, mut entry) = match data
.query(&SimplifiedQuery { .query(&SimplifiedQuery {
@ -418,6 +475,7 @@ async fn edit_request(
// update // update
entry.content = req.content; entry.content = req.content;
entry.metadata = req.metadata;
entry.edited = unix_epoch_timestamp(); entry.edited = unix_epoch_timestamp();
if let Err(e) = data if let Err(e) = data