diff --git a/CHANGELOG.md b/CHANGELOG.md index e9ae428..4fa843e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/lang/en/). +## [0.4.1] - 2026-03-26 +### Fixed +- Added support for rendering individual files, not just directories (e.g., + `mdpreview dir/file.md`). +- Fixed the rendering of Markdown lists. + + +--- + ## [0.4.0] - 2026-03-25 ### Added diff --git a/Cargo.lock b/Cargo.lock index 85aa0f9..f460700 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -982,7 +982,7 @@ checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" [[package]] name = "mdpreview" -version = "0.4.0" +version = "0.4.1" dependencies = [ "askama", "askama_axum", diff --git a/Cargo.toml b/Cargo.toml index 7a0130d..4e20698 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mdpreview" -version = "0.4.0" +version = "0.4.1" edition = "2024" authors = ["Vladislav Kan "] diff --git a/src/main.rs b/src/main.rs index fbe2356..5e6023d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -74,11 +74,11 @@ struct Args { #[arg(long, default_value_t = false)] browser: bool, - /// Open browser on random note. Requires flag --broswer + /// Open browser on random note. Requires flag --browser #[arg(long, default_value_t = false, requires = "browser")] random: bool, - /// Notes root + /// Notes root (can be a directory or a single file) #[arg()] root: PathBuf, } @@ -89,17 +89,20 @@ struct AppState { theme_set: Arc, tx: Arc>, root: PathBuf, + is_root_file: bool, } #[tokio::main] async fn main() { let args = Args::parse(); - if !args.root.is_dir() { - eprintln!("Root {} is not a directory", args.root.display()); + if !args.root.exists() { + eprintln!("Root path {} does not exist", args.root.display()); std::process::exit(1); } + let is_root_file = args.root.is_file(); + tracing_subscriber::registry() .with(tracing_subscriber::EnvFilter::new( std::env::var("RUST_LOG").unwrap_or_else(|_| "info,tower_http=warn".into()), @@ -117,6 +120,7 @@ async fn main() { theme_set: Arc::new(ts), tx: Arc::new(tx), root: args.root.clone(), + is_root_file, }; let watcher_state = state.clone(); @@ -129,7 +133,7 @@ async fn main() { .route("/random", get(random_file)) .route("/*path", get(serve_file)) .route("/events/*path", get(sse_handler)) - .with_state(state) + .with_state(state.clone()) .layer(TraceLayer::new_for_http()); let addr = resolve_addr(&args.host, args.port).expect("Failed to resolve address"); @@ -142,7 +146,9 @@ async fn main() { if args.browser { let mut url = format!("http://{actual_addr}"); if args.random { - url = format!("{url}/random") + if !state.is_root_file { + url = format!("{url}/random") + } } let _ = webbrowser::open(url.as_str()); } @@ -159,16 +165,84 @@ fn resolve_addr(host: &str, port: u16) -> io::Result { } 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(), + if state.is_root_file { + match render_single_file(&state.root, &state).await { + Ok(template) => template.into_response(), + Err(e) => e.into_response(), + } + } else { + match render_directory_index(&state.root, "").await { + Ok(template) => template.into_response(), + Err(e) => e.into_response(), + } } } +async fn render_single_file( + file_path: &StdPath, + state: &AppState, +) -> Result { + let metadata = match fs::metadata(file_path).await { + Ok(m) => m, + Err(_) => return Err(StatusCode::NOT_FOUND), + }; + + if metadata.is_dir() { + return Err(StatusCode::BAD_REQUEST); + } + + let extension = file_path + .extension() + .and_then(|ext| ext.to_str()) + .unwrap_or("") + .to_lowercase(); + + let content = match fs::read_to_string(file_path).await { + Ok(c) => c, + Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR), + }; + + let display_path = ""; + + let html_content = if extension == "md" { + markdown_to_html(&content, &state.syntax_set, &state.theme_set, display_path) + } else { + code_to_html( + &content, + extension.as_str(), + &state.syntax_set, + &state.theme_set, + ) + }; + + let filename = file_path + .file_name() + .and_then(OsStr::to_str) + .unwrap_or("Unknown") + .to_string(); + + let back_link = "/".to_string(); + + let sse_url = "/events/".to_string(); + let copy_path = format!("note fe {filename}").to_string(); + + Ok(NoteTemplate { + filename, + back_link, + content: html_content, + sse_url, + copy_path, + }) +} + async fn serve_file( State(state): State, AxumPath(full_path): AxumPath, ) -> impl IntoResponse { + if state.is_root_file { + return Err(StatusCode::NOT_FOUND); + } + if full_path.is_empty() { return Err(StatusCode::NOT_FOUND); } @@ -337,13 +411,23 @@ async fn sse_handler( let rx = state.tx.subscribe(); let requested_path = full_path.clone(); + let root_path_str = state.root.to_string_lossy().to_string(); let stream = BroadcastStream::new(rx).filter_map(move |res| { let req_path = requested_path.clone(); + let root_str = root_path_str.clone(); + let is_root_file_mode = state.is_root_file; + async move { match res { Ok(changed_path) => { - if changed_path.contains(&req_path) || changed_path.ends_with(&req_path) { + let should_notify = if is_root_file_mode { + changed_path == root_str || changed_path.ends_with(&root_str) + } else { + changed_path.contains(&req_path) || changed_path.ends_with(&req_path) + }; + + if should_notify { Some(Ok::( axum::response::sse::Event::default() .event("reload") @@ -368,6 +452,10 @@ async fn sse_handler( } async fn random_file(State(state): State) -> impl IntoResponse { + if state.is_root_file { + return Ok(Redirect::temporary("/")); + } + let mut files: Vec = Vec::new(); let mut stack = vec![state.root.clone()]; @@ -427,7 +515,13 @@ async fn run_file_watcher(state: AppState) { ) .expect("Failed to create watcher"); - if let Err(e) = watcher.watch(&state.root, RecursiveMode::Recursive) { + let watch_result = if state.is_root_file { + watcher.watch(&state.root, RecursiveMode::NonRecursive) + } else { + watcher.watch(&state.root, RecursiveMode::Recursive) + }; + + if let Err(e) = watch_result { eprintln!("Failed set watcher: {e}"); return; }