diff --git a/Cargo.lock b/Cargo.lock index c515104..264c86f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -104,6 +104,12 @@ dependencies = [ "serde", ] +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.11.0" @@ -141,6 +147,21 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "deranged" version = "0.5.6" @@ -166,6 +187,17 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "filetime" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +dependencies = [ + "cfg-if", + "libc", + "libredox", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -197,6 +229,30 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.32" @@ -204,6 +260,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -212,6 +269,40 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + [[package]] name = "futures-task" version = "0.3.32" @@ -224,8 +315,13 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ + "futures-channel", "futures-core", + "futures-io", + "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "slab", ] @@ -336,12 +432,52 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inotify" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + [[package]] name = "itoa" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "kqueue" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -354,6 +490,17 @@ version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +[[package]] +name = "libredox" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +dependencies = [ + "bitflags 2.11.0", + "libc", + "redox_syscall 0.7.1", +] + [[package]] name = "linked-hash-map" version = "0.5.6" @@ -395,11 +542,13 @@ name = "md-renderer" version = "0.1.0" dependencies = [ "axum", + "futures", + "notify", "pulldown-cmark", "serde", - "serde_json", "syntect", "tokio", + "tokio-stream", "tracing-subscriber", ] @@ -425,6 +574,18 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + [[package]] name = "mio" version = "1.1.1" @@ -436,6 +597,25 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "notify" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" +dependencies = [ + "bitflags 2.11.0", + "crossbeam-channel", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio 0.8.11", + "walkdir", + "windows-sys 0.48.0", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -463,7 +643,7 @@ version = "6.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0" dependencies = [ - "bitflags", + "bitflags 2.11.0", "libc", "once_cell", "onig_sys", @@ -497,7 +677,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", "windows-link", ] @@ -560,7 +740,7 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b" dependencies = [ - "bitflags", + "bitflags 2.11.0", "getopts", "memchr", "unicase", @@ -590,7 +770,16 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.11.0", +] + +[[package]] +name = "redox_syscall" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35985aa610addc02e24fc232012c86fd11f14111180f902b67e2d5331f8ebf2b" +dependencies = [ + "bitflags 2.11.0", ] [[package]] @@ -862,7 +1051,7 @@ checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" dependencies = [ "bytes", "libc", - "mio", + "mio 1.1.1", "parking_lot", "pin-project-lite", "signal-hook-registry", @@ -882,6 +1071,31 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "tower" version = "0.5.3" @@ -1015,13 +1229,22 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets", + "windows-targets 0.53.5", ] [[package]] @@ -1033,6 +1256,21 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.53.5" @@ -1040,28 +1278,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ "windows-link", - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.53.1" @@ -1074,24 +1330,48 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.53.1" diff --git a/Cargo.toml b/Cargo.toml index 78d7333..dadd3ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,8 @@ pulldown-cmark = "0.9" axum = "0.7" tokio = { version = "1", features = ["full"] } serde = { version = "1", features = ["derive"] } -serde_json = "1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } syntect = "5.0" +notify = "6.1" # Для слежения за файлами +tokio-stream = { version = "0.1", features = ["sync"] } +futures = "0.3" # Утилиты для стримов diff --git a/src/main.rs b/src/main.rs index e15962a..c8c4b6f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,14 +2,20 @@ use axum::{ extract::{Path, State}, routing::get, Router, - http::StatusCode, - response::Html, + http::{StatusCode, HeaderMap, header}, + response::{Html, Sse, IntoResponse}, }; use pulldown_cmark::{Parser, Options, html, Event, Tag, CodeBlockKind}; use std::net::SocketAddr; use std::path::PathBuf; use std::sync::Arc; use tokio::fs; +use tokio::sync::broadcast; +use tokio_stream::wrappers::BroadcastStream; +use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher, EventKind}; +use futures::StreamExt; +use std::convert::Infallible; +use std::time::Duration; // Импорт для syntect use syntect::easy::HighlightLines; @@ -17,33 +23,42 @@ use syntect::highlighting::ThemeSet; use syntect::html::{styled_line_to_highlighted_html, IncludeBackground}; use syntect::parsing::SyntaxSet; -/// Храним тяжелые ресурсы в состоянии приложения, чтобы не грузить их при каждом запросе +/// Храним тяжелые ресурсы и канал для уведомлений #[derive(Clone)] struct AppState { syntax_set: Arc, theme_set: Arc, + tx: Arc>, } #[tokio::main] async fn main() { - // Инициализация логгера tracing_subscriber::fmt::init(); - // Загружаем синтаксисы и темы ОДИН РАЗ при старте - // Это может занять несколько сотен миллисекунд, поэтому делаем это до запуска сервера println!("Загрузка баз синтаксисов и тем..."); let ss = SyntaxSet::load_defaults_newlines(); let ts = ThemeSet::load_defaults(); + // Создаем канал broadcast для SSE + let (tx, _rx) = broadcast::channel::(100); + let state = AppState { syntax_set: Arc::new(ss), theme_set: Arc::new(ts), + tx: Arc::new(tx), }; + // Запускаем глобальный вотчер в отдельной задаче + let watcher_state = state.clone(); + tokio::spawn(async move { + run_file_watcher(watcher_state).await; + }); + let app = Router::new() .route("/", get(root)) .route("/*path", get(serve_file)) - .with_state(state); // Передаем состояние в роутер + .route("/events/*path", get(sse_handler)) + .with_state(state); let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); println!("Сервер запущен на http://{}", addr); @@ -57,6 +72,49 @@ async fn root() -> Html<&'static str> { Html("

Markdown Server

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

") } +async fn sse_handler( + State(state): State, + Path(full_path): Path, +) -> impl IntoResponse { + let mut headers = HeaderMap::new(); + headers.insert(header::CONTENT_TYPE, header::HeaderValue::from_static("text/event-stream")); + headers.insert(header::CACHE_CONTROL, header::HeaderValue::from_static("no-cache")); + headers.insert(header::CONNECTION, header::HeaderValue::from_static("keep-alive")); + + let rx = state.tx.subscribe(); + let requested_path = full_path.clone(); + + let stream = BroadcastStream::new(rx) + .filter_map(move |res| { + let req_path = requested_path.clone(); + async move { + match res { + Ok(changed_path) => { + if changed_path.contains(&req_path) { + // Явно указываем типы: Ok + Some(Ok::( + axum::response::sse::Event::default() + .event("reload") + .data("") + )) + } else { + None + } + } + Err(_) => None, + } + } + }); + + let sse = Sse::new(stream) + .keep_alive( + axum::response::sse::KeepAlive::new() + .interval(Duration::from_secs(15)) + .text("ping") + ); + + (headers, sse) +} async fn serve_file( State(state): State, Path(full_path): Path, @@ -65,17 +123,14 @@ async fn serve_file( return Err(StatusCode::NOT_FOUND); } - // --- БЕЗОПАСНАЯ ПРОВЕРКА ПУТИ --- 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), }; - // Убеждаемся, что файл лежит внутри папки ./notes let base_dir = match fs::canonicalize("./notes").await { Ok(p) => p, Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR), @@ -86,7 +141,6 @@ async fn serve_file( return Err(StatusCode::FORBIDDEN); } - // Чтение файла let content = match fs::read_to_string(&safe_path).await { Ok(c) => c, Err(e) => { @@ -95,15 +149,50 @@ async fn serve_file( } }; - // Конвертация Markdown -> HTML с подсветкой - let html_content = markdown_to_html(&content, &state.syntax_set, &state.theme_set); + let html_content = markdown_to_html(&content, &state.syntax_set, &state.theme_set, &full_path); Ok(Html(html_content)) } -/// Основная функция конвертации -fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet) -> String { - // Выбираем тему (base16-ocean.dark отлично смотрится) +/// Запуск наблюдателя за файловой системой +async fn run_file_watcher(state: AppState) { + // Создаем канал MPSC для передачи путей из колбэка notify в основной цикл + let (tx_fs, mut rx_fs) = tokio::sync::mpsc::channel::(100); + + // Настраиваем notify watcher + // Перемещаем tx_fs внутрь замыкания через clone + let tx_fs_clone = tx_fs.clone(); + let mut watcher = RecommendedWatcher::new( + move |res: Result| { + if let Ok(event) = res { + if matches!(event.kind, EventKind::Modify(_)) { + for path in event.paths { + // Используем клонированный отправитель внутри замыкания + let _ = tx_fs_clone.blocking_send(path); + } + } + } + }, + Config::default(), + ).expect("Failed to create watcher"); + + let watch_path = PathBuf::from("./notes"); + if let Err(e) = watcher.watch(&watch_path, RecursiveMode::Recursive) { + eprintln!("Ошибка настройки watcher: {}", e); + return; + } + + println!("Watcher запущен для директории: {:?}", watch_path); + + // Цикл обработки событий от файловой системы + while let Some(path) = rx_fs.recv().await { + if let Some(path_str) = path.to_str() { + let _ = state.tx.send(path_str.to_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(); @@ -115,10 +204,7 @@ fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet) -> String { let parser = Parser::new_ext(markdown, options); - // --- ЭТАП 1: Обработка событий для замены блоков кода --- - // Мы собираем новые события, где блоки кода заменены на сырой HTML с подсветкой 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(); @@ -137,13 +223,26 @@ fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet) -> String { Event::End(Tag::CodeBlock(_)) => { in_code_block = false; - // Генерируем подсвеченный HTML + // Исправленная подсветка синтаксиса (построчно) 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 regions = h.highlight(¤t_code, ss); - styled_line_to_highlighted_html(®ions[..], IncludeBackground::No) - .unwrap_or_else(|_| escape_html(¤t_code)) + let mut result_html = String::new(); + + // Разбиваем код на строки и подсвечиваем каждую отдельно + for line in current_code.lines() { + // Добавляем перевод строки обратно, так как lines() удаляет его + let line_with_newline = format!("{}\n", line); + 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) } @@ -151,21 +250,16 @@ fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet) -> String { escape_html(¤t_code) }; - // Формируем тег
...
с нашими стилями let full_html = format!( r#"
{}
"#, highlighted_html ); - - // Вставляем как сырое HTML событие processed_events.push(Event::Html(full_html.into())); }, Event::Text(text) if in_code_block => { - // Накопление текста кода (не добавляем в events сразу) current_code.push_str(&text); }, _ => { - // Все остальные события (заголовки, параграфы, обычный текст) пропускаем как есть if !in_code_block { processed_events.push(event); } @@ -173,11 +267,12 @@ fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet) -> String { } } - // --- ЭТАП 2: Рендеринг итогового HTML --- let mut body_html = String::new(); html::push_html(&mut body_html, processed_events.into_iter()); - // Оборачиваем в полный HTML документ с темной темой + let sse_url = format!("/events/{}", file_path); + + // Обратите внимание на двойные фигурные скобки {{ и }} для экранирования в format! format!( r#" @@ -186,24 +281,8 @@ fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet) -> String { Markdown Preview
{}
+ + "#, - body_html + body_html, + sse_url ) } -// Функция экранирования HTML для случаев, когда синтаксис не найден или это простой блок кода fn escape_html(text: &str) -> String { text.replace('&', "&") .replace('<', "<")