add: entry metadata
This commit is contained in:
parent
d80368e6c2
commit
b505199492
11 changed files with 631 additions and 45 deletions
186
Cargo.lock
generated
186
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -178,6 +178,10 @@ video {
|
|||
appearance: none;
|
||||
}
|
||||
|
||||
.button.small {
|
||||
--h: 28px;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background: var(--color-super-raised);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
})
|
||||
}"))
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
})
|
||||
}"))
|
||||
|
|
|
@ -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 }}"))
|
||||
|
||||
|
|
|
@ -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"))
|
||||
|
|
264
src/model.rs
264
src/model.rs
|
@ -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>"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue