diff --git a/src/main.rs b/src/main.rs index ef62ead..34fc9ef 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,6 @@ use axum::{ use futures::StreamExt; use mime_guess::from_path; use notify::{Config, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; -use pulldown_cmark::{CodeBlockKind, Event, Options, Tag, html}; use rand::seq::SliceRandom; use std::convert::Infallible; use std::fmt::Write; @@ -20,9 +19,7 @@ use tokio::fs; use tokio::sync::broadcast; use tokio_stream::wrappers::BroadcastStream; -use syntect::easy::HighlightLines; use syntect::highlighting::ThemeSet; -use syntect::html::{IncludeBackground, styled_line_to_highlighted_html}; use syntect::parsing::SyntaxSet; use tower_http::trace::TraceLayer; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; @@ -30,7 +27,9 @@ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; use clap::Parser; use std::io; -// Загрузка шаблонов при компиляции +mod markdown; +use markdown::markdown_to_html; + const TEMPLATE_FILE: &str = include_str!("../templates/file.html"); const TEMPLATE_DIR: &str = include_str!("../templates/dir.html"); @@ -101,7 +100,7 @@ async fn main() { let addr = resolve_addr(&args.host, args.port).unwrap(); - println!("Сервер запущен на http://{addr}"); + println!("Server started on {addr}"); let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); axum::serve(listener, app).await.unwrap(); @@ -181,7 +180,6 @@ async fn serve_file( 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); }; @@ -191,30 +189,24 @@ async fn serve_file( }; if !safe_path.starts_with(&base_dir) { - eprintln!( - "Попытка выхода за пределы директории: {}", - safe_path.display() - ); + eprintln!("Path traversal: {}", safe_path.display()); return Err(StatusCode::FORBIDDEN); } let metadata = match fs::metadata(&safe_path).await { Ok(m) => m, Err(e) => { - eprintln!("Ошибка получения метаданных: {e}"); - return Err(StatusCode::NOT_FOUND); // Лучше NOT_FOUND для отсутствующих файлов + eprintln!("Error getting metadata: {e}"); + return Err(StatusCode::NOT_FOUND); } }; - // 1. Если это директория - рендерим индекс if metadata.is_dir() { return render_directory_index(&safe_path, &full_path) .await .map(|h| h.into_response()); } - // 2. ПРОВЕРКА НА ИЗОБРАЖЕНИЯ (И другую статику) - // Распространенные расширения изображений let extension = safe_path .extension() .and_then(|ext| ext.to_str()) @@ -225,30 +217,24 @@ async fn serve_file( ); if is_image { - // Читаем файл в байты let file_content = match fs::read(&safe_path).await { Ok(content) => content, Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR), }; - // Определяем MIME тип let mime_type = from_path(&safe_path).first_or_octet_stream(); - // Возвращаем файл с правильным заголовком Content-Type return Ok(([(header::CONTENT_TYPE, mime_type.as_ref())], file_content).into_response()); } - // 3. Если это не картинка и не папка, считаем что это Markdown (или текст) - // Читаем контент как строку let content = match fs::read_to_string(&safe_path).await { Ok(c) => c, Err(e) => { - eprintln!("Ошибка чтения: {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() { @@ -260,22 +246,13 @@ async fn serve_file( "/".to_string() }; - let back_button_html = format!( - r#"
- - Назад - -
"# - ); - let html_content = markdown_to_html(&content, &state.syntax_set, &state.theme_set, &full_path); // Заполнение шаблона let final_html = TEMPLATE_FILE .replace("{{CONTENT}}", &html_content) .replace("{{SSE_URL}}", &format!("/events/{full_path}")) - .replace("{{BACK_BUTTON}}", &back_button_html); + .replace("{{BACK_LINK}}", &back_link); Ok(Html(final_html).into_response()) } @@ -287,7 +264,7 @@ async fn render_directory_index( let mut entries = match fs::read_dir(dir_path).await { Ok(list) => list, Err(e) => { - eprintln!("Ошибка чтения директории: {e}"); + eprintln!("Error directory reading: {e}"); return Err(StatusCode::FORBIDDEN); } }; @@ -388,7 +365,7 @@ async fn run_file_watcher(state: AppState) { ) .expect("Failed to create watcher"); - let watch_path = PathBuf::from(state.root); + let watch_path = state.root; if let Err(e) = watcher.watch(&watch_path, RecursiveMode::Recursive) { eprintln!("Ошибка настройки watcher: {e}"); return; @@ -401,120 +378,15 @@ async fn run_file_watcher(state: AppState) { } } -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(); - options.insert(Options::ENABLE_TABLES); - options.insert(Options::ENABLE_FOOTNOTES); - options.insert(Options::ENABLE_STRIKETHROUGH); - options.insert(Options::ENABLE_TASKLISTS); - options.insert(Options::ENABLE_SMART_PUNCTUATION); - - let parser = pulldown_cmark::Parser::new_ext(markdown, options); - - let mut processed_events: Vec = Vec::new(); - let mut in_code_block = false; - let mut current_lang: Option = None; - let mut current_code = String::new(); - - for event in parser { - match event { - Event::Start(Tag::CodeBlock(kind)) => { - in_code_block = true; - current_code.clear(); - current_lang = if let CodeBlockKind::Fenced(l) = kind { - Some(l.to_string()) - } else { - None - }; - } - Event::End(Tag::CodeBlock(_)) => { - in_code_block = false; - let is_mermaid = current_lang.as_deref() == Some("mermaid"); - - if is_mermaid { - let escaped_code = escape_html(¤t_code); - let mermaid_html = format!( - r#"
-
- Mermaid Diagram - -
-
{escaped_code}
-
"# - ); - 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); - - let highlighted_html = if let Some(lang) = ¤t_lang { - 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!("{line}\n"); - match h.highlight_line(&line_with_newline, ss) { - Ok(regions) => { - let html_line = styled_line_to_highlighted_html( - ®ions[..], - IncludeBackground::No, - ) - .unwrap_or_else(|_| escape_html(&line_with_newline)); - result_html.push_str(&html_line); - } - Err(_) => { - result_html.push_str(&escape_html(&line_with_newline)); - } - } - } - result_html - } else { - escape_html(¤t_code) - } - } else { - escape_html(¤t_code) - }; - - let code_container = format!( - r#"
-
- {lang_escaped} - -
-
{highlighted_html}
-
"# - ); - processed_events.push(Event::Html(code_container.into())); - } - } - Event::Text(text) if in_code_block => { - current_code.push_str(&text); - } - _ => { - if !in_code_block { - processed_events.push(event); - } - } - } - } - - let mut body_html = String::new(); - html::push_html(&mut body_html, processed_events.into_iter()); - body_html -} - async fn random_file(State(state): State) -> Result { 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, // Пропускаем недоступные папки + Err(_) => continue, }; while let Some(entry) = entries @@ -524,7 +396,6 @@ async fn random_file(State(state): State) -> Result) -> Result String { - text.replace('&', "&") - .replace('<', "<") - .replace('>', ">") - .replace('"', """) - .replace('\'', "'") -} diff --git a/src/markdown.rs b/src/markdown.rs new file mode 100644 index 0000000..2a5abf6 --- /dev/null +++ b/src/markdown.rs @@ -0,0 +1,117 @@ +use pulldown_cmark::{CodeBlockKind, Event, Options, Tag, html}; +use syntect::easy::HighlightLines; +use syntect::highlighting::ThemeSet; +use syntect::html::{IncludeBackground, styled_line_to_highlighted_html}; +use syntect::parsing::SyntaxSet; + +pub 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(); + options.insert(Options::ENABLE_TABLES); + options.insert(Options::ENABLE_FOOTNOTES); + options.insert(Options::ENABLE_STRIKETHROUGH); + options.insert(Options::ENABLE_TASKLISTS); + options.insert(Options::ENABLE_SMART_PUNCTUATION); + + let parser = pulldown_cmark::Parser::new_ext(markdown, options); + + let mut processed_events: Vec = Vec::new(); + let mut in_code_block = false; + let mut current_lang: Option = None; + let mut current_code = String::new(); + + for event in parser { + match event { + Event::Start(Tag::CodeBlock(kind)) => { + in_code_block = true; + current_code.clear(); + current_lang = if let CodeBlockKind::Fenced(l) = kind { + Some(l.to_string()) + } else { + None + }; + } + Event::End(Tag::CodeBlock(_)) => { + in_code_block = false; + let is_mermaid = current_lang.as_deref() == Some("mermaid"); + + if is_mermaid { + let escaped_code = escape_html(¤t_code); + let mermaid_html = format!( + r#"
+
+ Mermaid Diagram + +
+
{escaped_code}
+
"# + ); + 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); + + let highlighted_html = if let Some(lang) = ¤t_lang { + 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!("{line}\n"); + match h.highlight_line(&line_with_newline, ss) { + Ok(regions) => { + let html_line = styled_line_to_highlighted_html( + ®ions[..], + IncludeBackground::No, + ) + .unwrap_or_else(|_| escape_html(&line_with_newline)); + result_html.push_str(&html_line); + } + Err(_) => { + result_html.push_str(&escape_html(&line_with_newline)); + } + } + } + result_html + } else { + escape_html(¤t_code) + } + } else { + escape_html(¤t_code) + }; + + let code_container = format!( + r#"
+
+ {lang_escaped} + +
+
{highlighted_html}
+
"# + ); + processed_events.push(Event::Html(code_container.into())); + } + } + Event::Text(text) if in_code_block => { + current_code.push_str(&text); + } + _ => { + if !in_code_block { + processed_events.push(event); + } + } + } + } + + let mut body_html = String::new(); + html::push_html(&mut body_html, processed_events.into_iter()); + body_html +} + +fn escape_html(text: &str) -> String { + text.replace('&', "&") + .replace('<', "<") + .replace('>', ">") + .replace('"', """) + .replace('\'', "'") +} diff --git a/templates/file.html b/templates/file.html index 950e6d1..87ec8e5 100644 --- a/templates/file.html +++ b/templates/file.html @@ -46,7 +46,12 @@
- {{BACK_BUTTON}} + {{CONTENT}}