From b3e04d7581247020dc7066ddafe66ca774eeafe0 Mon Sep 17 00:00:00 2001 From: thek4n Date: Fri, 20 Feb 2026 17:56:19 +0300 Subject: [PATCH] refactor to templates --- src/main.rs | 275 ++++++++------------------------------------ templates/dir.html | 32 ++++++ templates/file.html | 90 +++++++++++++++ 3 files changed, 173 insertions(+), 224 deletions(-) create mode 100644 templates/dir.html create mode 100644 templates/file.html diff --git a/src/main.rs b/src/main.rs index 305e155..8572a38 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,6 +22,10 @@ use syntect::highlighting::ThemeSet; use syntect::html::{styled_line_to_highlighted_html, IncludeBackground}; use syntect::parsing::SyntaxSet; +// Загрузка шаблонов при компиляции +const TEMPLATE_FILE: &str = include_str!("../templates/file.html"); +const TEMPLATE_DIR: &str = include_str!("../templates/dir.html"); + #[derive(Clone)] struct AppState { syntax_set: Arc, @@ -37,7 +41,6 @@ async fn main() { let ss = SyntaxSet::load_defaults_newlines(); let ts = ThemeSet::load_defaults(); - // Создаем канал broadcast для SSE let (tx, _rx) = broadcast::channel::(100); let state = AppState { @@ -46,7 +49,6 @@ async fn main() { tx: Arc::new(tx), }; - // Запускаем глобальный вотчер в отдельной задаче let watcher_state = state.clone(); tokio::spawn(async move { run_file_watcher(watcher_state).await; @@ -66,8 +68,8 @@ async fn main() { axum::serve(listener, app).await.unwrap(); } -async fn root() -> Html<&'static str> { - Html("

Markdown Server

Перейдите на /files/example.md

") +async fn root() -> Result, StatusCode> { + render_directory_index(&PathBuf::from("./notes"), "").await } async fn sse_handler( @@ -124,6 +126,7 @@ async fn serve_file( let mut requested_path = PathBuf::from("./notes"); requested_path.push(&full_path); + // Безопасность путей let safe_path = match fs::canonicalize(&requested_path).await { Ok(p) => p, Err(_) => return Err(StatusCode::NOT_FOUND), @@ -148,7 +151,7 @@ async fn serve_file( }; if metadata.is_dir() { - return render_directory_index(&safe_path, &full_path).await; + return render_directory_index(&safe_path, &full_path).await; } let content = match fs::read_to_string(&safe_path).await { @@ -159,9 +162,33 @@ async fn serve_file( } }; + // Логика кнопки "Назад" + 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 back_button_html = format!( + r#"
+ + Назад + +
"#, + back_link + ); + let html_content = markdown_to_html(&content, &state.syntax_set, &state.theme_set, &full_path); - Ok(Html(html_content)) + // Заполнение шаблона + let final_html = TEMPLATE_FILE + .replace("{{CONTENT}}", &html_content) + .replace("{{SSE_URL}}", &format!("/events/{}", full_path)) + .replace("{{BACK_BUTTON}}", &back_button_html); + + Ok(Html(final_html)) } async fn render_directory_index( @@ -176,19 +203,17 @@ async fn render_directory_index( } }; - let mut files: Vec<(String, String, bool)> = Vec::new(); // (name, link, is_dir) + 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('/'); @@ -198,25 +223,23 @@ async fn render_directory_index( files.push((file_name, link_path, is_dir)); } - // Сортировка: сначала директории, потом файлы, по алфавиту files.sort_by(|a, b| { match (a.2, b.2) { - (true, false) => std::cmp::Ordering::Less, // Директории первыми + (true, false) => std::cmp::Ordering::Less, (false, true) => std::cmp::Ordering::Greater, - _ => a.0.cmp(&b.0), // Затем по имени + _ => a.0.cmp(&b.0), } }); - let mut list_html = String::from("
    "); + let mut list_html = String::from("
      "); - // Добавляем ссылку на родительскую директорию, если мы не в корне - if request_path != "files" && !request_path.is_empty() { + if !request_path.is_empty() && request_path != "files" { let parent_path = request_path.rsplit_once('/').map(|(p, _)| p).unwrap_or(""); let parent_link = if parent_path.is_empty() { "/".to_string() } else { format!("/{}", parent_path) }; list_html.push_str(&format!( - r#"
    • - 📁 .. + r#"
    • + 📁 ..
    • "#, parent_link )); @@ -224,63 +247,29 @@ async fn render_directory_index( for (name, link, is_dir) in files { let icon = if is_dir { "📁" } else { "📄" }; - // Новый цвет: светло-голубой для файлов и папок - let color = "#64b5f6"; - let hover_color = "#90caf9"; // Чуть светлее при наведении - list_html.push_str(&format!( - r#"
    • - - {} + r#"
    • + + {} {}
    • "#, link.trim_start_matches('/'), - color, - hover_color, - color, icon, name )); } list_html.push_str("
    "); - let html_content = format!( - r#" - - - - - Обзор директории - - - -
    -

    📂 Обзор директории: /{}

    - {} -

    - ← На главную -

    -
    - -"#, - request_path.trim_start_matches('/'), - list_html - ); + let title_path = if request_path.is_empty() { "" } else { request_path.trim_start_matches('/') }; - Ok(Html(html_content)) + 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); @@ -313,7 +302,7 @@ async fn run_file_watcher(state: AppState) { } } -fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet, file_path: &str) -> String { +fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet, _file_path: &str) -> String { let theme = &ts.themes["base16-ocean.dark"]; let mut options = Options::empty(); @@ -343,13 +332,9 @@ fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet, file_path: &s }, Event::End(Tag::CodeBlock(_)) => { in_code_block = false; - - // Проверка на Mermaid let is_mermaid = current_lang.as_deref() == Some("mermaid"); if is_mermaid { - // Для Mermaid просто экранируем контент и оборачиваем в div - // Кнопка копирования тоже нужна let escaped_code = escape_html(¤t_code); let mermaid_html = format!( r#"
    @@ -363,7 +348,6 @@ fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet, file_path: &s ); processed_events.push(Event::Html(mermaid_html.into())); } else { - // Обычная обработка кода с подсветкой let lang_display = current_lang.as_deref().unwrap_or("text"); let lang_escaped = escape_html(lang_display); @@ -371,7 +355,6 @@ fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet, file_path: &s if let Some(syntax) = ss.find_syntax_by_token(lang) { let mut h = HighlightLines::new(syntax, theme); let mut result_html = String::new(); - for line in current_code.lines() { let line_with_newline = format!("{}\n", line); match h.highlight_line(&line_with_newline, ss) { @@ -402,7 +385,6 @@ fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet, file_path: &s lang_escaped, highlighted_html ); - processed_events.push(Event::Html(code_container.into())); } }, @@ -419,162 +401,7 @@ fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet, file_path: &s let mut body_html = String::new(); html::push_html(&mut body_html, processed_events.into_iter()); - - let sse_url = format!("/events/{}", file_path); - - format!( - r#" - - - - - Markdown Preview - - - - - -
    - {} -
    - - - -"#, - body_html, - sse_url - ) + body_html } fn escape_html(text: &str) -> String { diff --git a/templates/dir.html b/templates/dir.html new file mode 100644 index 0000000..5baae97 --- /dev/null +++ b/templates/dir.html @@ -0,0 +1,32 @@ + + + + + + Обзор директории + + + +
    +

    📂 Обзор директории: /{{TITLE_PATH}}

    + {{FILE_LIST}} + +
    + + diff --git a/templates/file.html b/templates/file.html new file mode 100644 index 0000000..ab2815f --- /dev/null +++ b/templates/file.html @@ -0,0 +1,90 @@ + + + + + + Markdown Preview + + + + +
    + {{BACK_BUTTON}} + {{CONTENT}} +
    + + + +