add sse to watch file changes

This commit is contained in:
thek4n 2026-02-20 03:07:24 +03:00
parent f49895c5f8
commit 40bf0f413b
3 changed files with 452 additions and 72 deletions

308
Cargo.lock generated
View File

@ -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"

View File

@ -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" # Утилиты для стримов

View File

@ -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<SyntaxSet>,
theme_set: Arc<ThemeSet>,
tx: Arc<broadcast::Sender<String>>,
}
#[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::<String>(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("<h1 style='color:white; text-align:center;'>Markdown Server</h1><p style='color:#aaa; text-align:center;'>Перейдите на <a href='/files/example.md'>/files/example.md</a></p>")
}
async fn sse_handler(
State(state): State<AppState>,
Path(full_path): Path<String>,
) -> 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<Event, Infallible>
Some(Ok::<axum::response::sse::Event, Infallible>(
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<AppState>,
Path(full_path): Path<String>,
@ -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::<PathBuf>(100);
// Настраиваем notify watcher
// Перемещаем tx_fs внутрь замыкания через clone
let tx_fs_clone = tx_fs.clone();
let mut watcher = RecommendedWatcher::new(
move |res: Result<notify::Event, notify::Error>| {
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<Event> = Vec::new();
let mut in_code_block = false;
let mut current_lang: Option<String> = 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) = &current_lang {
if let Some(syntax) = ss.find_syntax_by_token(lang) {
let mut h = HighlightLines::new(syntax, theme);
let regions = h.highlight(&current_code, ss);
styled_line_to_highlighted_html(&regions[..], IncludeBackground::No)
.unwrap_or_else(|_| escape_html(&current_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(&regions[..], 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(&current_code)
}
@ -151,21 +250,16 @@ fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet) -> String {
escape_html(&current_code)
};
// Формируем тег <pre><code>...</code></pre> с нашими стилями
let full_html = format!(
r#"<pre style="background-color: #2b303b; border-radius: 6px; padding: 15px; overflow-x: auto; margin: 1em 0; border: 1px solid #444;"><code>{}</code></pre>"#,
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#"<!DOCTYPE html>
<html lang="ru">
@ -186,24 +281,8 @@ fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet) -> String {
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Markdown Preview</title>
<style>
body {{
background-color: #121212;
color: #e0e0e0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
margin: 0;
padding: 40px 20px;
display: flex;
justify-content: center;
line-height: 1.6;
}}
.content {{
max-width: 800px;
width: 100%;
background-color: #1e1e1e;
padding: 40px;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0,0,0,0.5);
}}
body {{ background-color: #121212; color: #e0e0e0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; margin: 0; padding: 40px 20px; display: flex; justify-content: center; line-height: 1.6; }}
.content {{ max-width: 800px; width: 100%; background-color: #1e1e1e; padding: 40px; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.5); }}
a {{ color: #bb86fc; text-decoration: none; }}
a:hover {{ text-decoration: underline; }}
h1, h2, h3, h4 {{ color: #ffffff; margin-top: 1.5em; margin-bottom: 0.5em; }}
@ -213,26 +292,45 @@ fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet) -> String {
th {{ background-color: #2c2c2c; }}
blockquote {{ border-left: 4px solid #bb86fc; margin: 1em 0; padding-left: 1em; color: #aaa; background: #252525; padding: 10px; }}
code {{ font-family: 'Consolas', 'Monaco', monospace; }}
/* Стили для инлайн кода внутри текста */
p > code, li > code {{
background-color: #2c2c2c;
padding: 2px 6px;
border-radius: 4px;
color: #ff79c6;
}}
p > code, li > code {{ background-color: #2c2c2c; padding: 2px 6px; border-radius: 4px; color: #ff79c6; }}
#status {{ position: fixed; top: 10px; right: 10px; padding: 5px 10px; border-radius: 4px; font-size: 12px; font-weight: bold; }}
.connected {{ background-color: #2ecc71; color: #000; }}
.disconnected {{ background-color: #e74c3c; color: #fff; }}
.reconnecting {{ background-color: #f1c40f; color: #000; }}
</style>
</head>
<body>
<div class="content">
{}
</div>
<script>
const sseUrl = "{}";
function connect() {{
const evtSource = new EventSource(sseUrl);
evtSource.onerror = (err) => {{
evtSource.close();
setTimeout(connect, 3000);
}};
evtSource.addEventListener("reload", (event) => {{
console.log("Получено событие обновления");
location.reload();
}});
}}
connect();
</script>
</body>
</html>"#,
body_html
body_html,
sse_url
)
}
// Функция экранирования HTML для случаев, когда синтаксис не найден или это простой блок кода
fn escape_html(text: &str) -> String {
text.replace('&', "&amp;")
.replace('<', "&lt;")