From d6a1a3e67bb863ce68be3aa8814b91f504c95717 Mon Sep 17 00:00:00 2001 From: thek4n Date: Fri, 20 Feb 2026 03:12:25 +0300 Subject: [PATCH] add copy button --- src/main.rs | 116 ++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 103 insertions(+), 13 deletions(-) diff --git a/src/main.rs b/src/main.rs index c8c4b6f..e4db6a8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -91,7 +91,6 @@ async fn sse_handler( match res { Ok(changed_path) => { if changed_path.contains(&req_path) { - // Явно указываем типы: Ok Some(Ok::( axum::response::sse::Event::default() .event("reload") @@ -115,6 +114,7 @@ async fn sse_handler( (headers, sse) } + async fn serve_file( State(state): State, Path(full_path): Path, @@ -156,18 +156,14 @@ async fn serve_file( /// Запуск наблюдателя за файловой системой 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); } } @@ -184,7 +180,6 @@ async fn run_file_watcher(state: AppState) { 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()); @@ -223,15 +218,19 @@ fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet, file_path: &s Event::End(Tag::CodeBlock(_)) => { in_code_block = false; - // Исправленная подсветка синтаксиса (построчно) + // Определяем отображаемое имя языка + let lang_display = current_lang.as_deref().unwrap_or("text"); + + // Экранируем имя языка для HTML атрибута и текста + let lang_escaped = escape_html(lang_display); + + // Подсветка синтаксиса (построчно) 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 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) => { @@ -250,11 +249,21 @@ fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet, file_path: &s escape_html(¤t_code) }; - let full_html = format!( - r#"
{}
"#, + // Формируем HTML с заголовком и кнопкой копирования + // Мы экранируем current_code еще раз для data-атрибута, хотя для копирования будем брать текст из pre + let code_container = format!( + r#"
+
+ {} + +
+
{}
+
"#, + lang_escaped, highlighted_html ); - processed_events.push(Event::Html(full_html.into())); + + processed_events.push(Event::Html(code_container.into())); }, Event::Text(text) if in_code_block => { current_code.push_str(&text); @@ -272,7 +281,6 @@ fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet, file_path: &s let sse_url = format!("/events/{}", file_path); - // Обратите внимание на двойные фигурные скобки {{ и }} для экранирования в format! format!( r#" @@ -292,8 +300,62 @@ fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet, file_path: &s 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; }} + /* Стили для блоков кода с подсветкой */ + .code-block-wrapper {{ + margin: 1em 0; + border: 1px solid #444; + border-radius: 6px; + overflow: hidden; + background-color: #2b303b; + }} + .code-header {{ + display: flex; + justify-content: space-between; + align-items: center; + background-color: #232730; + padding: 6px 12px; + border-bottom: 1px solid #444; + font-size: 0.85em; + color: #a0a0a0; + }} + .code-lang {{ + font-weight: bold; + text-transform: uppercase; + letter-spacing: 0.5px; + }} + .copy-btn {{ + background: transparent; + border: 1px solid #555; + color: #ccc; + padding: 2px 8px; + border-radius: 4px; + cursor: pointer; + font-size: 0.8em; + transition: all 0.2s; + }} + .copy-btn:hover {{ + background-color: #444; + color: #fff; + border-color: #777; + }} + .copy-btn:active {{ + transform: scale(0.95); + }} + pre {{ + padding: 15px; + overflow-x: auto; + margin: 0; + }} + pre code {{ + background: transparent; + padding: 0; + color: inherit; + }} + #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; }} @@ -323,6 +385,34 @@ fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet, file_path: &s }} connect(); + + function copyCode(button) {{ + // Находим обертку, затем pre внутри неё + const wrapper = button.closest('.code-block-wrapper'); + if (!wrapper) return; + + const pre = wrapper.querySelector('pre'); + if (!pre) return; + + // Получаем текстовое содержимое (без HTML тегов подсветки) + const codeText = pre.innerText; + + navigator.clipboard.writeText(codeText).then(() => {{ + const originalText = button.innerText; + button.innerText = 'Copied!'; + button.style.borderColor = '#2ecc71'; + button.style.color = '#2ecc71'; + + setTimeout(() => {{ + button.innerText = originalText; + button.style.borderColor = ''; + button.style.color = ''; + }}, 2000); + }}).catch(err => {{ + console.error('Ошибка копирования:', err); + button.innerText = 'Error'; + }}); + }} "#,