fix: support for single-file rendering
This commit is contained in:
parent
845d8c9475
commit
b990725af1
@ -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
|
||||
|
||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -982,7 +982,7 @@ checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
|
||||
|
||||
[[package]]
|
||||
name = "mdpreview"
|
||||
version = "0.4.0"
|
||||
version = "0.4.1"
|
||||
dependencies = [
|
||||
"askama",
|
||||
"askama_axum",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "mdpreview"
|
||||
version = "0.4.0"
|
||||
version = "0.4.1"
|
||||
edition = "2024"
|
||||
authors = ["Vladislav Kan <thek4n@yandex.ru>"]
|
||||
|
||||
|
||||
116
src/main.rs
116
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<ThemeSet>,
|
||||
tx: Arc<broadcast::Sender<String>>,
|
||||
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<SocketAddr> {
|
||||
}
|
||||
|
||||
async fn root_handler(State(state): State<AppState>) -> 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<NoteTemplate, StatusCode> {
|
||||
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<AppState>,
|
||||
AxumPath(full_path): AxumPath<String>,
|
||||
) -> 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, Infallible>(
|
||||
axum::response::sse::Event::default()
|
||||
.event("reload")
|
||||
@ -368,6 +452,10 @@ async fn sse_handler(
|
||||
}
|
||||
|
||||
async fn random_file(State(state): State<AppState>) -> impl IntoResponse {
|
||||
if state.is_root_file {
|
||||
return Ok(Redirect::temporary("/"));
|
||||
}
|
||||
|
||||
let mut files: Vec<PathBuf> = 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;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user