add: better layout

This commit is contained in:
trisua 2025-07-25 15:12:15 -04:00
parent 2cc9ed7445
commit dbd70d9592
19 changed files with 451 additions and 87 deletions

View file

@ -7,7 +7,7 @@ use axum::{Extension, Router};
use nanoneo::core::element::Render;
use std::{collections::HashMap, env::var, net::SocketAddr, process::exit, sync::Arc};
use tera::{Tera, Value};
use tetratto_core::sdk::DataClient;
use tetratto_core::{html, sdk::DataClient};
use tetratto_shared::hash::salt;
use tokio::sync::RwLock;
use tower_http::{
@ -64,20 +64,29 @@ async fn main() {
// build lisp
create_dir_if_not_exists!("./templates_build");
create_dir_if_not_exists!("./icons");
for x in glob::glob("./templates_src/**/*").expect("failed to read pattern") {
match x {
Ok(x) => std::fs::write(
x.to_str()
.unwrap()
.replace("templates_src/", "templates_build/"),
nanoneo::parse(&std::fs::read_to_string(x).expect("failed to read template"))
.render(&mut HashMap::new()),
html::pull_icons(
nanoneo::parse(&std::fs::read_to_string(x).expect("failed to read template"))
.render(&mut HashMap::new()),
"./icons",
)
.await,
)
.expect("failed to write template"),
Err(e) => panic!("{e}"),
}
}
// create docs dir
create_dir_if_not_exists!("./docs");
// ...
let mut tera = match Tera::new(&format!("./templates_build/**/*")) {
Ok(t) => t,

View file

@ -76,7 +76,7 @@ fn parse_text_color_line(output: &mut String, buffer: &mut String, line: &str) {
// by this point, we have: !
// %color_buffer%main_buffer%%
output.push_str(&format!(
"<span style=\"color: {color_buffer}\">{buffer}</span>"
"<span style=\"color: {color_buffer}\" class=\"color_block\">{buffer}</span>"
));
color_buffer.clear();
@ -468,7 +468,14 @@ fn parse_image_line(output: &mut String, buffer: &mut String, line: &str) {
if in_image {
// end
output.push_str(&format!(
"<img loading=\"lazy\" alt=\"{alt}\" src=\"{buffer}\" />"
"<img loading=\"lazy\" alt=\"{alt}\" src=\"{buffer}\" style=\"float: {}\" />",
if buffer.ends_with("#left") {
"left"
} else if buffer.ends_with("#right") {
"right"
} else {
"unset"
}
));
alt = String::new();

View file

@ -357,6 +357,17 @@ macro_rules! metadata_css {
}
};
($selector:literal, $property:literal !important, $self:ident.$field:ident->$output:ident) => {
if !$self.$field.is_empty() {
$output.push_str(&format!(
"{} {{ {}: {} !important; }}\n",
$selector,
$property,
EntryMetadata::css_escape(&$self.$field)
));
}
};
($selector:literal, $property:literal, $format:literal, $self:ident.$field:ident->$output:ident) => {
if !$self.$field.is_empty() {
$output.push_str(&format!(
@ -462,7 +473,7 @@ impl EntryMetadata {
metadata_css!(".container", "border-radius", self.container_border_radius->output);
metadata_css!(".container", "box-shadow", self.container_shadow->output);
metadata_css!(".container", "text-shadow", self.content_text_shadow->output);
metadata_css!(".container a", "color", self.content_text_link_color->output);
metadata_css!("*, html *", "--color-link" !important, self.content_text_link_color->output);
if self.content_text_align != TextAlignment::Left {
output.push_str(&format!(

View file

@ -8,6 +8,8 @@ use axum::{
response::{Html, IntoResponse},
routing::{get, get_service, post},
};
use axum_extra::extract::CookieJar;
use pathbufd::PathBufD;
use serde::Deserialize;
use serde_valid::Validate;
use tera::Context;
@ -32,6 +34,7 @@ pub fn routes() -> Router {
get_service(tower_http::services::ServeDir::new("./public")),
)
.fallback(not_found_request)
.route("/docs/{name}", get(view_doc_request))
// pages
.route("/", get(index_request))
.route("/{slug}", get(view_request))
@ -78,6 +81,39 @@ async fn index_request(Extension(data): Extension<State>) -> impl IntoResponse {
)
}
async fn view_doc_request(
Extension(data): Extension<State>,
Path(name): Path<String>,
) -> impl IntoResponse {
let (ref data, ref tera, ref build_code) = *data.read().await;
let path = PathBufD::current().extend(&["docs", &format!("{name}.md")]);
if !std::fs::exists(&path).unwrap_or(false) {
let mut ctx = default_context(&data, &build_code);
ctx.insert(
"error",
&Error::GeneralNotFound("entry".to_string()).to_string(),
);
return Html(tera.render("error.lisp", &ctx).unwrap());
}
let text = match std::fs::read_to_string(&path) {
Ok(t) => t,
Err(e) => {
let mut ctx = default_context(&data, &build_code);
ctx.insert("error", &Error::MiscError(e.to_string()).to_string());
return Html(tera.render("error.lisp", &ctx).unwrap());
}
};
let mut ctx = default_context(&data, &build_code);
ctx.insert("text", &text);
ctx.insert("file_name", &name);
return Html(tera.render("doc.lisp", &ctx).unwrap());
}
async fn view_request(
Extension(data): Extension<State>,
Path(slug): Path<String>,
@ -258,27 +294,46 @@ fn default_random() -> String {
salt()
}
/// The time that must be waited between each entry creation.
const CREATE_WAIT_TIME: usize = 5000;
async fn create_request(
jar: CookieJar,
Extension(data): Extension<State>,
Json(req): Json<CreateEntry>,
) -> impl IntoResponse {
) -> std::result::Result<impl IntoResponse, Json<ApiReturn<()>>> {
let (ref data, _, _) = *data.read().await;
// check wait time
if let Some(cookie) = jar.get("__Secure-Claim-Next") {
if unix_epoch_timestamp()
!= cookie
.to_string()
.replace("__Secure-Claim-Next=", "")
.parse::<usize>()
.unwrap_or(0)
{
return Err(Json(
Error::MiscError("You must wait a bit to create another entry".to_string()).into(),
));
}
}
// check lengths
if req.slug.len() < 2 {
return Json(Error::DataTooShort("slug".to_string()).into());
return Err(Json(Error::DataTooShort("slug".to_string()).into()));
}
if req.slug.len() > 32 {
return Json(Error::DataTooLong("slug".to_string()).into());
return Err(Json(Error::DataTooLong("slug".to_string()).into()));
}
if req.content.len() < 2 {
return Json(Error::DataTooShort("content".to_string()).into());
return Err(Json(Error::DataTooShort("content".to_string()).into()));
}
if req.content.len() > 150_000 {
return Json(Error::DataTooLong("content".to_string()).into());
return Err(Json(Error::DataTooLong("content".to_string()).into()));
}
// check slug
@ -288,17 +343,19 @@ async fn create_request(
.unwrap();
if regex.captures(&req.slug).is_some() {
return Json(Error::MiscError("This slug contains invalid characters".to_string()).into());
return Err(Json(
Error::MiscError("This slug contains invalid characters".to_string()).into(),
));
}
// check metadata
let metadata: EntryMetadata = match toml::from_str(&EntryMetadata::ini_to_toml(&req.metadata)) {
Ok(x) => x,
Err(e) => return Json(Error::MiscError(e.to_string()).into()),
Err(e) => return Err(Json(Error::MiscError(e.to_string()).into())),
};
if let Err(e) = metadata.validate() {
return Json(Error::MiscError(e.to_string()).into());
return Err(Json(Error::MiscError(e.to_string()).into()));
}
// check for existing
@ -310,7 +367,9 @@ async fn create_request(
.await
.is_ok()
{
return Json(Error::MiscError("Slug already in use".to_string()).into());
return Err(Json(
Error::MiscError("Slug already in use".to_string()).into(),
));
}
// create
@ -333,22 +392,31 @@ async fn create_request(
)
.await
{
return Json(e.into());
return Err(Json(e.into()));
}
if let Err(e) = data
.insert(format!("entries.views('{}')", req.slug), 0.to_string())
.await
{
return Json(e.into());
return Err(Json(e.into()));
}
// return
Json(ApiReturn {
ok: true,
message: "Success".to_string(),
payload: Some((req.slug, req.edit_code)),
})
Ok((
[(
"Set-Cookie",
format!(
"__Secure-Claim-Next={}; SameSite=Lax; Secure; Path=/; HostOnly=true; HttpOnly=true; Max-Age=5",
unix_epoch_timestamp() + CREATE_WAIT_TIME
),
)],
Json(ApiReturn {
ok: true,
message: "Success".to_string(),
payload: Some((req.slug, req.edit_code)),
}),
))
}
#[derive(Deserialize)]