From 4eb81598698c3f4dcf552368cd2b7008642b14c6 Mon Sep 17 00:00:00 2001 From: thek4n Date: Mon, 23 Mar 2026 23:24:18 +0300 Subject: [PATCH] chore(refactor): add templater --- Cargo.lock | 114 +++++++++- Cargo.toml | 14 +- src/main.rs | 494 +++++++++++++++++++++----------------------- src/other.rs | 10 - templates/base.html | 177 ++++++++++++++++ templates/dir.html | 125 +++-------- templates/file.html | 276 +++---------------------- 7 files changed, 589 insertions(+), 621 deletions(-) create mode 100644 templates/base.html diff --git a/Cargo.lock b/Cargo.lock index 794f2ec..60c09ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -67,6 +67,61 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "askama" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b79091df18a97caea757e28cd2d5fda49c6cd4bd01ddffd7ff01ace0c0ad2c28" +dependencies = [ + "askama_derive", + "askama_escape", + "humansize", + "num-traits", + "percent-encoding", +] + +[[package]] +name = "askama_axum" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a41603f7cdbf5ac4af60760f17253eb6adf6ec5b6f14a7ed830cf687d375f163" +dependencies = [ + "askama", + "axum-core", + "http", +] + +[[package]] +name = "askama_derive" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19fe8d6cb13c4714962c072ea496f3392015f0989b1a2847bb4b2d9effd71d83" +dependencies = [ + "askama_parser", + "basic-toml", + "mime", + "mime_guess", + "proc-macro2", + "quote", + "serde", + "syn", +] + +[[package]] +name = "askama_escape" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" + +[[package]] +name = "askama_parser" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acb1161c6b64d1c3d83108213c2a2533a342ac225aabd0bda218278c2ddb00c0" +dependencies = [ + "nom", +] + [[package]] name = "async-compression" version = "0.4.40" @@ -96,6 +151,12 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + [[package]] name = "axum" version = "0.7.9" @@ -157,6 +218,15 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "basic-toml" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" +dependencies = [ + "serde", +] + [[package]] name = "bincode" version = "1.3.3" @@ -528,6 +598,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "humansize" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] + [[package]] name = "hyper" version = "1.8.1" @@ -638,6 +717,12 @@ version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "libredox" version = "0.1.12" @@ -687,8 +772,10 @@ checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" [[package]] name = "mdpreview" -version = "0.1.2" +version = "0.1.3" dependencies = [ + "askama", + "askama_axum", "axum", "clap", "futures", @@ -726,6 +813,12 @@ dependencies = [ "unicase", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -759,6 +852,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "notify" version = "6.1.1" @@ -793,6 +896,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "once_cell" version = "1.21.3" diff --git a/Cargo.toml b/Cargo.toml index c165910..58b2a09 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,15 +1,15 @@ [package] name = "mdpreview" -version = "0.1.2" +version = "0.1.3" edition = "2024" authors = ["Vladislav Kan "] [profile.release] -opt-level = "z" # Оптимизация именно по размеру (z > s) -lto = true # Link Time Optimization: объединяет и оптимизирует весь код целиком -codegen-units = 1 # Уменьшает количество параллельных единиц компиляции, позволяя лучше оптимизировать -panic = "abort" # Отключает механизм развертывания стека при панике (экономит много места) -strip = true # Автоматически удаляет символы отладки (доступно в стабильной версии Rust 1.59+) +opt-level = "z" +lto = true +codegen-units = 1 +panic = "abort" +strip = true [dependencies] @@ -26,3 +26,5 @@ clap = { version = "4.5", features = ["derive"] } tower-http = { version = "0.6.8", features = ["trace", "compression-gzip", "cors"] } mime_guess = "2" rand = "0.8" +askama = { version = "0.12", features = ["with-axum"] } +askama_axum = "0.4" diff --git a/src/main.rs b/src/main.rs index ed9044d..113a548 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,9 @@ +use askama::Template; use axum::{ Router, - extract::{Path, State}, + extract::{Path as AxumPath, State}, http::{HeaderMap, StatusCode, header}, - response::{Html, IntoResponse, Sse}, + response::{IntoResponse, Redirect, Sse}, routing::get, }; use futures::StreamExt; @@ -11,9 +12,8 @@ use notify::{Config, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; use rand::seq::SliceRandom; use std::convert::Infallible; use std::ffi::OsStr; -use std::fmt::Write; use std::net::{SocketAddr, ToSocketAddrs}; -use std::path::PathBuf; +use std::path::{Path as StdPath, PathBuf}; use std::sync::Arc; use std::time::Duration; use tokio::fs; @@ -34,21 +34,38 @@ use markdown::markdown_to_html; mod other; use other::code_to_html; -const TEMPLATE_FILE: &str = include_str!("../templates/file.html"); -const TEMPLATE_DIR: &str = include_str!("../templates/dir.html"); +#[derive(Clone)] +pub struct FileEntry { + pub name: String, + pub link: String, + pub is_dir: bool, +} + +#[derive(Template)] +#[template(path = "dir.html")] +pub struct DirectoryTemplate { + pub title_path: String, + pub files: Vec, +} + +#[derive(Template)] +#[template(path = "file.html")] +pub struct NoteTemplate { + pub filename: String, + pub back_link: String, + pub content: String, + pub sse_url: String, +} #[derive(clap::Parser, Debug)] #[command(author, version, about, long_about = None)] struct Args { - /// Host to listen #[arg(long, default_value_t = String::from("localhost"))] host: String, - /// Port to listen #[arg(short, long, default_value_t = 8080)] port: u16, - /// Markdown documents directory root #[arg()] root: PathBuf, } @@ -66,13 +83,13 @@ async fn main() { let args = Args::parse(); if !args.root.is_dir() { - eprintln!("Root {root} is not a directory", root = args.root.display()); + eprintln!("Root {} is not a directory", args.root.display()); std::process::exit(1); } tracing_subscriber::registry() .with(tracing_subscriber::EnvFilter::new( - std::env::var("RUST_LOG").unwrap_or_else(|_| "info,tower_http=info".into()), + std::env::var("RUST_LOG").unwrap_or_else(|_| "info,tower_http=warn".into()), )) .with(tracing_subscriber::fmt::layer()) .init(); @@ -86,7 +103,7 @@ async fn main() { syntax_set: Arc::new(ss), theme_set: Arc::new(ts), tx: Arc::new(tx), - root: args.root, + root: args.root.clone(), }; let watcher_state = state.clone(); @@ -95,37 +112,188 @@ async fn main() { }); let app = Router::new() - .route("/", get(root)) + .route("/", get(root_handler)) .route("/random", get(random_file)) .route("/*path", get(serve_file)) .route("/events/*path", get(sse_handler)) .with_state(state) .layer(TraceLayer::new_for_http()); - let addr = resolve_addr(&args.host, args.port).unwrap(); - + let addr = resolve_addr(&args.host, args.port).expect("Failed to resolve address"); let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); - println!("Server started on {addr}"); + println!("🚀 Server started on http://{addr}"); + axum::serve(listener, app).await.unwrap(); } fn resolve_addr(host: &str, port: u16) -> io::Result { let addr_str = format!("{host}:{port}"); - addr_str .to_socket_addrs()? .next() .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Cannot resolve addr")) } -async fn root(State(state): State) -> Result, StatusCode> { - render_directory_index(&state.root, "").await +async fn root_handler(State(state): State) -> impl IntoResponse { + match render_directory_index(&state.root, "").await { + Ok(template) => template.into_response(), + Err(e) => e.into_response(), + } +} + +async fn serve_file( + State(state): State, + AxumPath(full_path): AxumPath, +) -> impl IntoResponse { + if full_path.is_empty() { + return Err(StatusCode::NOT_FOUND); + } + + let mut requested_path = state.root.clone(); + requested_path.push(&full_path); + + let Ok(safe_path) = fs::canonicalize(&requested_path).await else { + return Err(StatusCode::NOT_FOUND); + }; + + let Ok(base_dir) = fs::canonicalize(&state.root).await else { + return Err(StatusCode::INTERNAL_SERVER_ERROR); + }; + + if !safe_path.starts_with(&base_dir) { + eprintln!("Path traversal attempt: {}", safe_path.display()); + return Err(StatusCode::FORBIDDEN); + } + + let metadata = match fs::metadata(&safe_path).await { + Ok(m) => m, + Err(_) => return Err(StatusCode::NOT_FOUND), + }; + + if metadata.is_dir() { + return match render_directory_index(&safe_path, &full_path).await { + Ok(t) => Ok(t.into_response()), + Err(e) => Err(e), + }; + } + + let extension = safe_path + .extension() + .and_then(|ext| ext.to_str()) + .unwrap_or("") + .to_lowercase(); + + let is_image = matches!( + extension.as_str(), + "png" | "jpg" | "jpeg" | "gif" | "svg" | "webp" | "bmp" | "ico" + ); + + if is_image { + let file_content = match fs::read(&safe_path).await { + Ok(content) => content, + Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR), + }; + let mime_type = from_path(&safe_path).first_or_octet_stream(); + return Ok(([(header::CONTENT_TYPE, mime_type.as_ref())], file_content).into_response()); + } + + let content = match fs::read_to_string(&safe_path).await { + Ok(c) => c, + Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR), + }; + + let html_content = if extension == "md" { + markdown_to_html(&content, &state.syntax_set, &state.theme_set, &full_path) + } else { + code_to_html( + &content, + extension.as_str(), + &state.syntax_set, + &state.theme_set, + ) + }; + + let filename = safe_path + .file_name() + .and_then(OsStr::to_str) + .unwrap_or("Unknown") + .to_string(); + + let back_link = if let Some(pos) = full_path.rfind('/') { + let parent = &full_path[..pos]; + if parent.is_empty() { + "/".to_string() + } else { + format!("/{parent}") + } + } else { + "/".to_string() + }; + + let sse_url = format!("/events/{full_path}"); + + let template = NoteTemplate { + filename, + back_link, + content: html_content, + sse_url, + }; + + Ok(template.into_response()) +} + +async fn render_directory_index( + dir_path: &StdPath, + request_path: &str, +) -> Result { + let mut entries = match fs::read_dir(dir_path).await { + Ok(list) => list, + Err(_) => return Err(StatusCode::FORBIDDEN), + }; + + let mut files: Vec = Vec::new(); + + while let Some(entry) = entries + .next_entry() + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + { + let file_name = entry.file_name().to_string_lossy().to_string(); + + if file_name.starts_with('.') { + continue; + } + + let is_dir = entry.metadata().await.map(|m| m.is_dir()).unwrap_or(false); + + let link = if request_path.is_empty() { + file_name.clone() + } else { + format!("{}/{}", request_path, file_name) + }; + + files.push(FileEntry { + name: file_name, + link, + is_dir, + }); + } + + files.sort_by(|a, b| match (a.is_dir, b.is_dir) { + (true, false) => std::cmp::Ordering::Less, + (false, true) => std::cmp::Ordering::Greater, + _ => a.name.cmp(&b.name), + }); + + let title_path = request_path.trim_start_matches('/').to_string(); + + Ok(DirectoryTemplate { title_path, files }) } async fn sse_handler( State(state): State, - Path(full_path): Path, + AxumPath(full_path): AxumPath, ) -> impl IntoResponse { let mut headers = HeaderMap::new(); headers.insert( @@ -149,7 +317,7 @@ async fn sse_handler( async move { match res { Ok(changed_path) => { - if changed_path.contains(&req_path) { + if changed_path.contains(&req_path) || changed_path.ends_with(&req_path) { Some(Ok::( axum::response::sse::Event::default() .event("reload") @@ -173,250 +341,17 @@ async fn sse_handler( (headers, sse) } -async fn serve_file( - State(state): State, - Path(full_path): Path, -) -> Result { - if full_path.is_empty() { - return Err(StatusCode::NOT_FOUND); - } - - let mut requested_path = PathBuf::from(&state.root); - requested_path.push(&full_path); - - let Ok(safe_path) = fs::canonicalize(&requested_path).await else { - return Err(StatusCode::NOT_FOUND); - }; - - let Ok(base_dir) = fs::canonicalize(&state.root).await else { - return Err(StatusCode::INTERNAL_SERVER_ERROR); - }; - - if !safe_path.starts_with(&base_dir) { - eprintln!("Path traversal: {}", safe_path.display()); - return Err(StatusCode::FORBIDDEN); - } - - let metadata = match fs::metadata(&safe_path).await { - Ok(m) => m, - Err(e) => { - eprintln!("Error getting metadata: {e}"); - return Err(StatusCode::NOT_FOUND); - } - }; - - if metadata.is_dir() { - return render_directory_index(&safe_path, &full_path) - .await - .map(|h| h.into_response()); - } - - let extension = safe_path - .extension() - .and_then(|ext| ext.to_str()) - .unwrap_or(""); - let is_image = matches!( - extension.to_lowercase().as_str(), - "png" | "jpg" | "jpeg" | "gif" | "svg" | "webp" | "bmp" | "ico" - ); - - if is_image { - let file_content = match fs::read(&safe_path).await { - Ok(content) => content, - Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR), - }; - - let mime_type = from_path(&safe_path).first_or_octet_stream(); - - return Ok(([(header::CONTENT_TYPE, mime_type.as_ref())], file_content).into_response()); - } - - let content = match fs::read_to_string(&safe_path).await { - Ok(c) => c, - Err(e) => { - eprintln!("Error reading: {e}"); - return Err(StatusCode::INTERNAL_SERVER_ERROR); - } - }; - - let back_link = if let Some(pos) = full_path.rfind('/') { - let parent = &full_path[..pos]; - if parent.is_empty() { - "/".to_string() - } else { - format!("/{parent}") - } - } else { - "/".to_string() - }; - - let html_content = if let Some(extension) = safe_path.extension() { - if extension == OsStr::new("md") { - markdown_to_html(&content, &state.syntax_set, &state.theme_set, &full_path) - } else if let Some(ext_str) = extension.to_str() { - code_to_html(&content, ext_str, &state.syntax_set, &state.theme_set) - } else { - markdown_to_html(&content, &state.syntax_set, &state.theme_set, &full_path) - } - } else { - markdown_to_html(&content, &state.syntax_set, &state.theme_set, &full_path) - }; - - let filename: String = if let Some(filename) = PathBuf::from(&full_path).file_name() { - filename.to_str().unwrap_or("Markdown Preview").to_string() - } else { - "Markdown Preview".to_string() - }; - - // Заполнение шаблона - let final_html = TEMPLATE_FILE - .replace("{{TITLE}}", &filename) - .replace("{{CONTENT}}", &html_content) - .replace("{{SSE_URL}}", &format!("/events/{full_path}")) - .replace("{{BACK_LINK}}", &back_link); - - Ok(Html(final_html).into_response()) -} - -async fn render_directory_index( - dir_path: &PathBuf, - request_path: &str, -) -> Result, StatusCode> { - let mut entries = match fs::read_dir(dir_path).await { - Ok(list) => list, - Err(e) => { - eprintln!("Error directory reading: {e}"); - return Err(StatusCode::FORBIDDEN); - } - }; - - let mut files: Vec<(String, String, bool)> = Vec::new(); - - while let Some(entry) = entries - .next_entry() - .await - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? - { - let file_name = entry.file_name().to_string_lossy().to_string(); - - if file_name.starts_with('.') { - continue; - } - - let is_dir = entry.metadata().await.map(|m| m.is_dir()).unwrap_or(false); - - let mut link_path = request_path.to_string(); - if !link_path.ends_with('/') { - link_path.push('/'); - } - link_path.push_str(&file_name); - - files.push((file_name, link_path, is_dir)); - } - - files.sort_by(|a, b| match (a.2, b.2) { - (true, false) => std::cmp::Ordering::Less, - (false, true) => std::cmp::Ordering::Greater, - _ => a.0.cmp(&b.0), - }); - - let mut list_html = String::from("
    "); - - if !request_path.is_empty() && request_path != "files" { - let parent_path = request_path.rsplit_once('/').map_or("", |(p, _)| p); - let parent_link = if parent_path.is_empty() { - "/".to_string() - } else { - format!("/{parent_path}") - }; - - let _ = write!( - list_html, - r#"
  • - 📁 .. -
  • "# - ); - } - - for (name, link, is_dir) in files { - let icon = if is_dir { "📁" } else { "📄" }; - let _ = write!( - list_html, - r#"
  • - - {} - {} - -
  • "#, - link.trim_start_matches('/'), - icon, - name - ); - } - list_html.push_str("
"); - - let title_path = if request_path.is_empty() { - "" - } else { - request_path.trim_start_matches('/') - }; - - let final_html = TEMPLATE_DIR - .replace("{{TITLE_PATH}}", title_path) - .replace("{{FILE_LIST}}", &list_html); - - Ok(Html(final_html)) -} - -async fn run_file_watcher(state: AppState) { - let (tx_fs, mut rx_fs) = tokio::sync::mpsc::channel::(100); - - let tx_fs_clone = tx_fs.clone(); - let mut watcher = RecommendedWatcher::new( - move |res: Result| { - if let Ok(event) = res - && matches!(event.kind, EventKind::Modify(_)) - { - for path in event.paths { - let _ = tx_fs_clone.blocking_send(path); - } - } - }, - Config::default(), - ) - .expect("Failed to create watcher"); - - let watch_path = state.root; - if let Err(e) = watcher.watch(&watch_path, RecursiveMode::Recursive) { - eprintln!("Ошибка настройки watcher: {e}"); - return; - } - - while let Some(path) = rx_fs.recv().await { - if let Some(path_str) = path.to_str() { - let _ = state.tx.send(path_str.to_string()); - } - } -} - -async fn random_file(State(state): State) -> Result { +async fn random_file(State(state): State) -> impl IntoResponse { let mut files: Vec = Vec::new(); - let mut stack = vec![state.root.clone()]; while let Some(current_dir) = stack.pop() { - let mut entries = match fs::read_dir(¤t_dir).await { - Ok(list) => list, - Err(_) => continue, + let Ok(mut entries) = fs::read_dir(¤t_dir).await else { + continue; }; - while let Some(entry) = entries - .next_entry() - .await - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? - { + while let Ok(Some(entry)) = entries.next_entry().await { let path = entry.path(); - if entry.file_name().to_string_lossy().starts_with('.') { continue; } @@ -436,11 +371,44 @@ async fn random_file(State(state): State) -> Result p, + Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR), + }; let url_path = relative_path.to_string_lossy().replace('\\', "/"); - - Ok(axum::response::Redirect::temporary(&format!("/{url_path}"))) + Ok(Redirect::temporary(&format!("/{url_path}"))) +} + +async fn run_file_watcher(state: AppState) { + let (tx_fs, mut rx_fs) = tokio::sync::mpsc::channel::(100); + + let tx_fs_clone = tx_fs.clone(); + let mut watcher = RecommendedWatcher::new( + move |res: Result| { + if let Ok(event) = res + && matches!( + event.kind, + EventKind::Modify(_) | EventKind::Create(_) | EventKind::Remove(_) + ) + { + for path in event.paths { + let _ = tx_fs_clone.blocking_send(path); + } + } + }, + Config::default(), + ) + .expect("Failed to create watcher"); + + if let Err(e) = watcher.watch(&state.root, RecursiveMode::Recursive) { + eprintln!("Failed set watcher: {e}"); + return; + } + + while let Some(path) = rx_fs.recv().await { + if let Some(path_str) = path.to_str() { + let _ = state.tx.send(path_str.to_string()); + } + } } diff --git a/src/other.rs b/src/other.rs index 02f279c..0e4a66e 100644 --- a/src/other.rs +++ b/src/other.rs @@ -14,10 +14,8 @@ use syntect::parsing::SyntaxSet; /// # Возвращает /// Строку HTML, содержащую обертку блока кода с заголовком и кнопкой копирования. pub fn code_to_html(code: &str, lang: &str, ss: &SyntaxSet, ts: &ThemeSet) -> String { - // Используем тему по умолчанию из вашего примера let theme = &ts.themes["base16-ocean.dark"]; - // Проверка на Mermaid диаграммы if lang == "mermaid" { let escaped_code = escape_html(code); return format!( @@ -31,7 +29,6 @@ pub fn code_to_html(code: &str, lang: &str, ss: &SyntaxSet, ts: &ThemeSet) -> St ); } - // Логика подсветки синтаксиса let lang_display = if lang.is_empty() { "text" } else { lang }; let lang_escaped = escape_html(lang_display); @@ -40,36 +37,29 @@ pub fn code_to_html(code: &str, lang: &str, ss: &SyntaxSet, ts: &ThemeSet) -> St let mut h = HighlightLines::new(syntax, theme); let mut result_html = String::new(); - // Разбиваем код на строки для построчной подсветки for line in code.lines() { - // Добавляем перевод строки обратно, так как highlight_line ожидает полную строку let line_with_newline = format!("{line}\n"); match h.highlight_line(&line_with_newline, ss) { Ok(regions) => { - // styled_line_to_highlighted_html может вернуть ошибку, если что-то пойдет не так match styled_line_to_highlighted_html(®ions[..], IncludeBackground::No) { Ok(html_line) => result_html.push_str(&html_line), Err(_) => result_html.push_str(&escape_html(&line_with_newline)), } } Err(_) => { - // Если ошибка подсветки, просто экранируем строку result_html.push_str(&escape_html(&line_with_newline)); } } } result_html } else { - // Если язык не найден в наборе синтаксисов, просто экранируем весь код escape_html(code) } } else { - // Если язык не указан, просто экранируем escape_html(code) }; - // Формирование финального HTML контейнера format!( r#"
diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..ecd19e9 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,177 @@ + + + + + + {% block title %}Note{% endblock %} + + + + +
+
+ {% block header %} + + Back + + 🎲 Random note + {% endblock %} +
+ + {% block content %}{% endblock %} + + +
+ + + + diff --git a/templates/dir.html b/templates/dir.html index 2988ba7..3dd7862 100644 --- a/templates/dir.html +++ b/templates/dir.html @@ -1,91 +1,34 @@ - - - - - - Directory - - - -
-

📂 Directory: /{{TITLE_PATH}}

- {{FILE_LIST}} - -
- - +{% extends "base.html" %} + +{% block title %}Directory: /{{ title_path }}{% endblock %} + +{% block header %} +{% if !title_path.is_empty() %} + + Up + +{% else %} + 🏠 Root +{% endif %} +🎲 Random note +{% endblock %} + +{% block content %} +

📂 Directory: /{{ title_path }}

+ +{% endblock %} diff --git a/templates/file.html b/templates/file.html index a6af210..0f6c087 100644 --- a/templates/file.html +++ b/templates/file.html @@ -1,255 +1,31 @@ - - - - - - {{TITLE}} - - - - -
- - {{CONTENT}} - - -
- - - - - +function connect() { + const evtSource = new EventSource(sseUrl); + evtSource.onerror = (err) => { + evtSource.close(); + setTimeout(connect, 3000); + }; + evtSource.addEventListener("reload", (event) => { + console.log("Get new update event"); + location.reload(); + }); +} +connect(); +{% endblock %}