Compare commits
No commits in common. "main" and "0.4.0" have entirely different histories.
@ -1,8 +0,0 @@
|
|||||||
target/
|
|
||||||
.git/
|
|
||||||
.gitignore
|
|
||||||
Dockerfile
|
|
||||||
.dockerignore
|
|
||||||
README.md
|
|
||||||
.env
|
|
||||||
*.md
|
|
||||||
107
CHANGELOG.md
107
CHANGELOG.md
@ -1,107 +0,0 @@
|
|||||||
# MDPreview Changelog
|
|
||||||
All notable changes to this project will be documented in this file.
|
|
||||||
|
|
||||||
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/).
|
|
||||||
|
|
||||||
|
|
||||||
## v0.4.4 - 2026-03-29
|
|
||||||
### Fixed
|
|
||||||
- Fixed removing newlines.
|
|
||||||
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
|
|
||||||
## v0.4.3 - 2026-03-28
|
|
||||||
### Changed
|
|
||||||
- Change footer.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## v0.4.2 - 2026-03-27
|
|
||||||
### Added
|
|
||||||
- Added version and project name in footer.
|
|
||||||
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## v0.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.
|
|
||||||
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
|
|
||||||
## v0.4.0 - 2026-03-25
|
|
||||||
### Added
|
|
||||||
- Added a button to copy the edit command (`note edit current_note.md`).
|
|
||||||
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
|
|
||||||
## v0.3.0 - 2026-03-24
|
|
||||||
### Added
|
|
||||||
- Added the `--random` flag to open the browser on a random page when used with
|
|
||||||
`--browser` (`--browser --random`).
|
|
||||||
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
|
|
||||||
## v0.2.1 - 2026-03-23
|
|
||||||
### Fixed
|
|
||||||
- Fixed an issue where listening on port 0 (`--port 0`) caused the browser to
|
|
||||||
open the wrong port.
|
|
||||||
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
|
|
||||||
## v0.2.0 - 2026-03-23
|
|
||||||
### Added
|
|
||||||
- Added the `--browser` flag to automatically open the page in the browser.
|
|
||||||
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
|
|
||||||
## v0.1.3 - 2026-03-23
|
|
||||||
### Added
|
|
||||||
- Added a "Random File" button to the header.
|
|
||||||
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Non-Markdown files now render correctly.
|
|
||||||
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Translated the interface from Russian to English.
|
|
||||||
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
|
|
||||||
## v0.1.2 - 2026-03-22
|
|
||||||
### Added
|
|
||||||
- Added a "Random File" button.
|
|
||||||
- Added an "On Main" button.
|
|
||||||
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Changed the default port to 8080.
|
|
||||||
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
|
|
||||||
## v0.1.1 - 2026-03-21
|
|
||||||
### Added
|
|
||||||
- Added page titles.
|
|
||||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -982,7 +982,7 @@ checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mdpreview"
|
name = "mdpreview"
|
||||||
version = "0.4.4"
|
version = "0.4.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"askama",
|
"askama",
|
||||||
"askama_axum",
|
"askama_axum",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "mdpreview"
|
name = "mdpreview"
|
||||||
version = "0.4.4"
|
version = "0.4.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
authors = ["Vladislav Kan <thek4n@yandex.ru>"]
|
authors = ["Vladislav Kan <thek4n@yandex.ru>"]
|
||||||
|
|
||||||
|
|||||||
43
Dockerfile
43
Dockerfile
@ -1,43 +0,0 @@
|
|||||||
FROM rust:1.94 AS chef
|
|
||||||
|
|
||||||
RUN cargo install --locked cargo-chef
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY Cargo.toml Cargo.lock .
|
|
||||||
RUN mkdir src && touch src/main.rs
|
|
||||||
|
|
||||||
RUN cargo chef prepare --recipe-path recipe.json
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
FROM chef AS builder
|
|
||||||
|
|
||||||
COPY --from=chef /app/recipe.json recipe.json
|
|
||||||
|
|
||||||
RUN cargo chef cook --release --recipe-path recipe.json
|
|
||||||
|
|
||||||
|
|
||||||
COPY rust-toolchain.toml rust-toolchain.toml
|
|
||||||
|
|
||||||
RUN cargo fetch
|
|
||||||
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
RUN cargo build --release
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
FROM debian:bookworm-slim AS runtime
|
|
||||||
|
|
||||||
RUN useradd -m -s /bin/bash appuser
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY --from=builder /app/target/release/mdpreview /usr/local/bin/mdpreview
|
|
||||||
|
|
||||||
USER appuser
|
|
||||||
|
|
||||||
EXPOSE 8080
|
|
||||||
|
|
||||||
CMD ["mdpreview"]
|
|
||||||
4
TODO.md
4
TODO.md
@ -1,4 +1,4 @@
|
|||||||
# TODO <!-- [NOGREP] -->
|
# TODO <!-- [NOGREP] -->
|
||||||
|
|
||||||
* [X] Сделать кнопку скопировать путь
|
* [ ] Сделать кнопку скопировать путь
|
||||||
* [X] Сделать кнопку скопировать путь вместе с командой (`note edit ...`)
|
* [ ] Сделать кнопку скопировать путь вместе с командой (`note edit ...`)
|
||||||
|
|||||||
@ -1,3 +0,0 @@
|
|||||||
[toolchain]
|
|
||||||
channel = "stable"
|
|
||||||
components = ["clippy", "rustfmt"]
|
|
||||||
151
src/main.rs
151
src/main.rs
@ -2,10 +2,7 @@ use askama::Template;
|
|||||||
use axum::{
|
use axum::{
|
||||||
Router,
|
Router,
|
||||||
extract::{Path as AxumPath, State},
|
extract::{Path as AxumPath, State},
|
||||||
http::{
|
http::{HeaderMap, StatusCode, header},
|
||||||
HeaderMap, StatusCode,
|
|
||||||
header::{self},
|
|
||||||
},
|
|
||||||
response::{IntoResponse, Redirect, Sse},
|
response::{IntoResponse, Redirect, Sse},
|
||||||
routing::get,
|
routing::get,
|
||||||
};
|
};
|
||||||
@ -50,9 +47,6 @@ pub struct FileEntry {
|
|||||||
pub struct DirectoryTemplate {
|
pub struct DirectoryTemplate {
|
||||||
pub title_path: String,
|
pub title_path: String,
|
||||||
pub files: Vec<FileEntry>,
|
pub files: Vec<FileEntry>,
|
||||||
pub package_name: String,
|
|
||||||
pub authors: String,
|
|
||||||
pub version: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
@ -63,15 +57,8 @@ pub struct NoteTemplate {
|
|||||||
pub content: String,
|
pub content: String,
|
||||||
pub sse_url: String,
|
pub sse_url: String,
|
||||||
pub copy_path: String,
|
pub copy_path: String,
|
||||||
pub package_name: String,
|
|
||||||
pub authors: String,
|
|
||||||
pub version: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const PACKAGE_NAME: &str = env!("CARGO_PKG_NAME");
|
|
||||||
const AUTHORS: &str = env!("CARGO_PKG_AUTHORS");
|
|
||||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
|
||||||
|
|
||||||
#[derive(clap::Parser, Debug)]
|
#[derive(clap::Parser, Debug)]
|
||||||
#[command(author, version, about, long_about = None)]
|
#[command(author, version, about, long_about = None)]
|
||||||
struct Args {
|
struct Args {
|
||||||
@ -87,11 +74,11 @@ struct Args {
|
|||||||
#[arg(long, default_value_t = false)]
|
#[arg(long, default_value_t = false)]
|
||||||
browser: bool,
|
browser: bool,
|
||||||
|
|
||||||
/// Open browser on random note. Requires flag --browser
|
/// Open browser on random note. Requires flag --broswer
|
||||||
#[arg(long, default_value_t = false, requires = "browser")]
|
#[arg(long, default_value_t = false, requires = "browser")]
|
||||||
random: bool,
|
random: bool,
|
||||||
|
|
||||||
/// Notes root (can be a directory or a single file)
|
/// Notes root
|
||||||
#[arg()]
|
#[arg()]
|
||||||
root: PathBuf,
|
root: PathBuf,
|
||||||
}
|
}
|
||||||
@ -102,20 +89,17 @@ struct AppState {
|
|||||||
theme_set: Arc<ThemeSet>,
|
theme_set: Arc<ThemeSet>,
|
||||||
tx: Arc<broadcast::Sender<String>>,
|
tx: Arc<broadcast::Sender<String>>,
|
||||||
root: PathBuf,
|
root: PathBuf,
|
||||||
is_root_file: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
|
|
||||||
if !args.root.exists() {
|
if !args.root.is_dir() {
|
||||||
eprintln!("Root path {} does not exist", args.root.display());
|
eprintln!("Root {} is not a directory", args.root.display());
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
let is_root_file = args.root.is_file();
|
|
||||||
|
|
||||||
tracing_subscriber::registry()
|
tracing_subscriber::registry()
|
||||||
.with(tracing_subscriber::EnvFilter::new(
|
.with(tracing_subscriber::EnvFilter::new(
|
||||||
std::env::var("RUST_LOG").unwrap_or_else(|_| "info,tower_http=warn".into()),
|
std::env::var("RUST_LOG").unwrap_or_else(|_| "info,tower_http=warn".into()),
|
||||||
@ -133,7 +117,6 @@ async fn main() {
|
|||||||
theme_set: Arc::new(ts),
|
theme_set: Arc::new(ts),
|
||||||
tx: Arc::new(tx),
|
tx: Arc::new(tx),
|
||||||
root: args.root.clone(),
|
root: args.root.clone(),
|
||||||
is_root_file,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let watcher_state = state.clone();
|
let watcher_state = state.clone();
|
||||||
@ -146,7 +129,7 @@ async fn main() {
|
|||||||
.route("/random", get(random_file))
|
.route("/random", get(random_file))
|
||||||
.route("/*path", get(serve_file))
|
.route("/*path", get(serve_file))
|
||||||
.route("/events/*path", get(sse_handler))
|
.route("/events/*path", get(sse_handler))
|
||||||
.with_state(state.clone())
|
.with_state(state)
|
||||||
.layer(TraceLayer::new_for_http());
|
.layer(TraceLayer::new_for_http());
|
||||||
|
|
||||||
let addr = resolve_addr(&args.host, args.port).expect("Failed to resolve address");
|
let addr = resolve_addr(&args.host, args.port).expect("Failed to resolve address");
|
||||||
@ -154,14 +137,12 @@ async fn main() {
|
|||||||
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
||||||
|
|
||||||
let actual_addr = listener.local_addr().expect("Failed to get local address");
|
let actual_addr = listener.local_addr().expect("Failed to get local address");
|
||||||
println!("mdpreview v{VERSION} server started on http://{actual_addr}");
|
println!("Server started on http://{actual_addr}");
|
||||||
|
|
||||||
if args.browser {
|
if args.browser {
|
||||||
let mut url = format!("http://{actual_addr}");
|
let mut url = format!("http://{actual_addr}");
|
||||||
if args.random {
|
if args.random {
|
||||||
if !state.is_root_file {
|
url = format!("{url}/random")
|
||||||
url = format!("{url}/random")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
let _ = webbrowser::open(url.as_str());
|
let _ = webbrowser::open(url.as_str());
|
||||||
}
|
}
|
||||||
@ -178,87 +159,16 @@ fn resolve_addr(host: &str, port: u16) -> io::Result<SocketAddr> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn root_handler(State(state): State<AppState>) -> impl IntoResponse {
|
async fn root_handler(State(state): State<AppState>) -> impl IntoResponse {
|
||||||
if state.is_root_file {
|
match render_directory_index(&state.root, "").await {
|
||||||
match render_single_file(&state.root, &state).await {
|
Ok(template) => template.into_response(),
|
||||||
Ok(template) => template.into_response(),
|
Err(e) => e.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,
|
|
||||||
package_name: PACKAGE_NAME.to_string(),
|
|
||||||
authors: AUTHORS.to_string(),
|
|
||||||
version: VERSION.to_string(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn serve_file(
|
async fn serve_file(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
AxumPath(full_path): AxumPath<String>,
|
AxumPath(full_path): AxumPath<String>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
if state.is_root_file {
|
|
||||||
return Err(StatusCode::NOT_FOUND);
|
|
||||||
}
|
|
||||||
|
|
||||||
if full_path.is_empty() {
|
if full_path.is_empty() {
|
||||||
return Err(StatusCode::NOT_FOUND);
|
return Err(StatusCode::NOT_FOUND);
|
||||||
}
|
}
|
||||||
@ -354,9 +264,6 @@ async fn serve_file(
|
|||||||
content: html_content,
|
content: html_content,
|
||||||
sse_url,
|
sse_url,
|
||||||
copy_path,
|
copy_path,
|
||||||
package_name: PACKAGE_NAME.to_string(),
|
|
||||||
authors: AUTHORS.to_string(),
|
|
||||||
version: VERSION.to_string(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(template.into_response())
|
Ok(template.into_response())
|
||||||
@ -407,17 +314,7 @@ async fn render_directory_index(
|
|||||||
|
|
||||||
let title_path = request_path.trim_start_matches('/').to_string();
|
let title_path = request_path.trim_start_matches('/').to_string();
|
||||||
|
|
||||||
let package_name = env!("CARGO_PKG_NAME").to_string();
|
Ok(DirectoryTemplate { title_path, files })
|
||||||
let authors = env!("CARGO_PKG_AUTHORS").to_string();
|
|
||||||
let version = env!("CARGO_PKG_VERSION").to_string();
|
|
||||||
|
|
||||||
Ok(DirectoryTemplate {
|
|
||||||
title_path,
|
|
||||||
files,
|
|
||||||
package_name,
|
|
||||||
authors,
|
|
||||||
version,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn sse_handler(
|
async fn sse_handler(
|
||||||
@ -440,23 +337,13 @@ async fn sse_handler(
|
|||||||
|
|
||||||
let rx = state.tx.subscribe();
|
let rx = state.tx.subscribe();
|
||||||
let requested_path = full_path.clone();
|
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 stream = BroadcastStream::new(rx).filter_map(move |res| {
|
||||||
let req_path = requested_path.clone();
|
let req_path = requested_path.clone();
|
||||||
let root_str = root_path_str.clone();
|
|
||||||
let is_root_file_mode = state.is_root_file;
|
|
||||||
|
|
||||||
async move {
|
async move {
|
||||||
match res {
|
match res {
|
||||||
Ok(changed_path) => {
|
Ok(changed_path) => {
|
||||||
let should_notify = if is_root_file_mode {
|
if changed_path.contains(&req_path) || changed_path.ends_with(&req_path) {
|
||||||
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>(
|
Some(Ok::<axum::response::sse::Event, Infallible>(
|
||||||
axum::response::sse::Event::default()
|
axum::response::sse::Event::default()
|
||||||
.event("reload")
|
.event("reload")
|
||||||
@ -481,10 +368,6 @@ async fn sse_handler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn random_file(State(state): State<AppState>) -> impl IntoResponse {
|
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 files: Vec<PathBuf> = Vec::new();
|
||||||
let mut stack = vec![state.root.clone()];
|
let mut stack = vec![state.root.clone()];
|
||||||
|
|
||||||
@ -544,13 +427,7 @@ async fn run_file_watcher(state: AppState) {
|
|||||||
)
|
)
|
||||||
.expect("Failed to create watcher");
|
.expect("Failed to create watcher");
|
||||||
|
|
||||||
let watch_result = if state.is_root_file {
|
if let Err(e) = watcher.watch(&state.root, RecursiveMode::Recursive) {
|
||||||
watcher.watch(&state.root, RecursiveMode::NonRecursive)
|
|
||||||
} else {
|
|
||||||
watcher.watch(&state.root, RecursiveMode::Recursive)
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Err(e) = watch_result {
|
|
||||||
eprintln!("Failed set watcher: {e}");
|
eprintln!("Failed set watcher: {e}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,64 +4,8 @@ use syntect::highlighting::ThemeSet;
|
|||||||
use syntect::html::{IncludeBackground, styled_line_to_highlighted_html};
|
use syntect::html::{IncludeBackground, styled_line_to_highlighted_html};
|
||||||
use syntect::parsing::SyntaxSet;
|
use syntect::parsing::SyntaxSet;
|
||||||
|
|
||||||
fn normalize_lists(input: &str) -> String {
|
|
||||||
let mut result = String::with_capacity(input.len() + input.len() / 10);
|
|
||||||
|
|
||||||
for line in input.lines() {
|
|
||||||
if let Some(first_non_ws) = line.find(|c: char| !c.is_whitespace()) {
|
|
||||||
let marker_char = line.chars().nth(first_non_ws);
|
|
||||||
|
|
||||||
if let Some(marker) = marker_char {
|
|
||||||
if marker == '*' || marker == '-' || marker == '+' {
|
|
||||||
let rest_start = first_non_ws + 1;
|
|
||||||
|
|
||||||
if rest_start < line.len() {
|
|
||||||
let next_char = line.chars().nth(rest_start).unwrap();
|
|
||||||
|
|
||||||
if !next_char.is_whitespace() {
|
|
||||||
let mut marker_count = 1;
|
|
||||||
let mut is_only_markers = true;
|
|
||||||
|
|
||||||
for c in line.chars().skip(rest_start) {
|
|
||||||
if c == marker {
|
|
||||||
marker_count += 1;
|
|
||||||
} else if !c.is_whitespace() {
|
|
||||||
is_only_markers = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !(is_only_markers && marker_count >= 2) {
|
|
||||||
result.push_str(&line[..rest_start]);
|
|
||||||
result.push(' ');
|
|
||||||
result.push_str(&line[rest_start..]);
|
|
||||||
result.push('\n');
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
result.push_str(line);
|
|
||||||
result.push('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
if result.ends_with('\n') && !input.ends_with('\n') {
|
|
||||||
result.pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet, _file_path: &str) -> String {
|
pub fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet, _file_path: &str) -> String {
|
||||||
let normalized_markdown = normalize_lists(markdown);
|
let theme = &ts.themes["base16-ocean.dark"];
|
||||||
|
|
||||||
let theme = ts
|
|
||||||
.themes
|
|
||||||
.get("base16-ocean.dark")
|
|
||||||
.unwrap_or_else(|| ts.themes.values().next().expect("No themes available"));
|
|
||||||
|
|
||||||
let mut options = Options::empty();
|
let mut options = Options::empty();
|
||||||
options.insert(Options::ENABLE_TABLES);
|
options.insert(Options::ENABLE_TABLES);
|
||||||
@ -70,7 +14,7 @@ pub fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet, _file_pat
|
|||||||
options.insert(Options::ENABLE_TASKLISTS);
|
options.insert(Options::ENABLE_TASKLISTS);
|
||||||
options.insert(Options::ENABLE_SMART_PUNCTUATION);
|
options.insert(Options::ENABLE_SMART_PUNCTUATION);
|
||||||
|
|
||||||
let parser = pulldown_cmark::Parser::new_ext(&normalized_markdown, options);
|
let parser = pulldown_cmark::Parser::new_ext(markdown, options);
|
||||||
|
|
||||||
let mut processed_events: Vec<Event> = Vec::new();
|
let mut processed_events: Vec<Event> = Vec::new();
|
||||||
let mut in_code_block = false;
|
let mut in_code_block = false;
|
||||||
@ -79,10 +23,6 @@ pub fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet, _file_pat
|
|||||||
|
|
||||||
for event in parser {
|
for event in parser {
|
||||||
match event {
|
match event {
|
||||||
Event::Rule => {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Event::Start(Tag::CodeBlock(kind)) => {
|
Event::Start(Tag::CodeBlock(kind)) => {
|
||||||
in_code_block = true;
|
in_code_block = true;
|
||||||
current_code.clear();
|
current_code.clear();
|
||||||
@ -116,24 +56,19 @@ pub fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet, _file_pat
|
|||||||
if let Some(syntax) = ss.find_syntax_by_token(lang) {
|
if let Some(syntax) = ss.find_syntax_by_token(lang) {
|
||||||
let mut h = HighlightLines::new(syntax, theme);
|
let mut h = HighlightLines::new(syntax, theme);
|
||||||
let mut result_html = String::new();
|
let mut result_html = String::new();
|
||||||
|
for line in current_code.lines() {
|
||||||
let lines: Vec<&str> = current_code.lines().collect();
|
let line_with_newline = format!("{line}\n");
|
||||||
|
match h.highlight_line(&line_with_newline, ss) {
|
||||||
for line in lines.iter() {
|
|
||||||
match h.highlight_line(&line, ss) {
|
|
||||||
Ok(regions) => {
|
Ok(regions) => {
|
||||||
let html_line = styled_line_to_highlighted_html(
|
let html_line = styled_line_to_highlighted_html(
|
||||||
®ions[..],
|
®ions[..],
|
||||||
IncludeBackground::No,
|
IncludeBackground::No,
|
||||||
)
|
)
|
||||||
.unwrap_or_else(|_| escape_html(&line));
|
.unwrap_or_else(|_| escape_html(&line_with_newline));
|
||||||
|
|
||||||
result_html.push_str(&html_line);
|
result_html.push_str(&html_line);
|
||||||
result_html.push('\n');
|
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
result_html.push_str(&escape_html(&line));
|
result_html.push_str(&escape_html(&line_with_newline));
|
||||||
result_html.push('\n');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -169,7 +104,6 @@ pub fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet, _file_pat
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut body_html = String::new();
|
let mut body_html = String::new();
|
||||||
|
|
||||||
html::push_html(&mut body_html, processed_events.into_iter());
|
html::push_html(&mut body_html, processed_events.into_iter());
|
||||||
body_html
|
body_html
|
||||||
}
|
}
|
||||||
|
|||||||
@ -113,40 +113,24 @@
|
|||||||
}
|
}
|
||||||
.back-link span { margin-right: 8px; font-size: 1.2em; }
|
.back-link span { margin-right: 8px; font-size: 1.2em; }
|
||||||
img {
|
img {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
display: block;
|
display: block;
|
||||||
margin: 1.5em auto;
|
margin: 1.5em auto;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||||
}
|
}
|
||||||
ul {
|
/* Стили для списка файлов */
|
||||||
list-style-type: disc;
|
ul { list-style-type: none; padding: 0; margin: 0; }
|
||||||
padding: 0;
|
li { padding: 10px 0; border-bottom: 1px solid #333; }
|
||||||
margin: 0;
|
|
||||||
padding-left: 1.5em;
|
|
||||||
}
|
|
||||||
ol {
|
|
||||||
padding-left: 1.5em;
|
|
||||||
list-style-type: decimal;
|
|
||||||
}
|
|
||||||
li {
|
|
||||||
padding: .5em 0;
|
|
||||||
}
|
|
||||||
ul > li::marker, ol > li::marker { display: inline-block; }
|
|
||||||
.file-link {
|
.file-link {
|
||||||
color: #64b5f6;
|
color: #64b5f6;
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
.file-link:hover {
|
.file-link:hover { color: #90caf9; }
|
||||||
color: #90caf9;
|
.icon { margin-right: 10px; min-width: 24px; }
|
||||||
}
|
|
||||||
.icon {
|
|
||||||
margin-right: 10px;
|
|
||||||
min-width: 24px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@ -165,9 +149,6 @@
|
|||||||
<div class="footer">
|
<div class="footer">
|
||||||
<a href="/">← Main</a>
|
<a href="/">← Main</a>
|
||||||
<a href="/random">🎲 Random note</a>
|
<a href="/random">🎲 Random note</a>
|
||||||
{% block footer %}
|
|
||||||
<p>{{ package_name }} v{{ version }}</p>
|
|
||||||
{% endblock %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user