chore(refactor): refactor
This commit is contained in:
parent
65a59ce59c
commit
ba2e11f07c
166
src/main.rs
166
src/main.rs
@ -8,7 +8,6 @@ use axum::{
|
|||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use mime_guess::from_path;
|
use mime_guess::from_path;
|
||||||
use notify::{Config, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
|
use notify::{Config, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
|
||||||
use pulldown_cmark::{CodeBlockKind, Event, Options, Tag, html};
|
|
||||||
use rand::seq::SliceRandom;
|
use rand::seq::SliceRandom;
|
||||||
use std::convert::Infallible;
|
use std::convert::Infallible;
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
@ -20,9 +19,7 @@ use tokio::fs;
|
|||||||
use tokio::sync::broadcast;
|
use tokio::sync::broadcast;
|
||||||
use tokio_stream::wrappers::BroadcastStream;
|
use tokio_stream::wrappers::BroadcastStream;
|
||||||
|
|
||||||
use syntect::easy::HighlightLines;
|
|
||||||
use syntect::highlighting::ThemeSet;
|
use syntect::highlighting::ThemeSet;
|
||||||
use syntect::html::{IncludeBackground, styled_line_to_highlighted_html};
|
|
||||||
use syntect::parsing::SyntaxSet;
|
use syntect::parsing::SyntaxSet;
|
||||||
use tower_http::trace::TraceLayer;
|
use tower_http::trace::TraceLayer;
|
||||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||||
@ -30,7 +27,9 @@ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
|||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use std::io;
|
use std::io;
|
||||||
|
|
||||||
// Загрузка шаблонов при компиляции
|
mod markdown;
|
||||||
|
use markdown::markdown_to_html;
|
||||||
|
|
||||||
const TEMPLATE_FILE: &str = include_str!("../templates/file.html");
|
const TEMPLATE_FILE: &str = include_str!("../templates/file.html");
|
||||||
const TEMPLATE_DIR: &str = include_str!("../templates/dir.html");
|
const TEMPLATE_DIR: &str = include_str!("../templates/dir.html");
|
||||||
|
|
||||||
@ -101,7 +100,7 @@ async fn main() {
|
|||||||
|
|
||||||
let addr = resolve_addr(&args.host, args.port).unwrap();
|
let addr = resolve_addr(&args.host, args.port).unwrap();
|
||||||
|
|
||||||
println!("Сервер запущен на http://{addr}");
|
println!("Server started on {addr}");
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
||||||
axum::serve(listener, app).await.unwrap();
|
axum::serve(listener, app).await.unwrap();
|
||||||
@ -181,7 +180,6 @@ async fn serve_file(
|
|||||||
let mut requested_path = PathBuf::from(&state.root);
|
let mut requested_path = PathBuf::from(&state.root);
|
||||||
requested_path.push(&full_path);
|
requested_path.push(&full_path);
|
||||||
|
|
||||||
// Безопасность путей
|
|
||||||
let Ok(safe_path) = fs::canonicalize(&requested_path).await else {
|
let Ok(safe_path) = fs::canonicalize(&requested_path).await else {
|
||||||
return Err(StatusCode::NOT_FOUND);
|
return Err(StatusCode::NOT_FOUND);
|
||||||
};
|
};
|
||||||
@ -191,30 +189,24 @@ async fn serve_file(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if !safe_path.starts_with(&base_dir) {
|
if !safe_path.starts_with(&base_dir) {
|
||||||
eprintln!(
|
eprintln!("Path traversal: {}", safe_path.display());
|
||||||
"Попытка выхода за пределы директории: {}",
|
|
||||||
safe_path.display()
|
|
||||||
);
|
|
||||||
return Err(StatusCode::FORBIDDEN);
|
return Err(StatusCode::FORBIDDEN);
|
||||||
}
|
}
|
||||||
|
|
||||||
let metadata = match fs::metadata(&safe_path).await {
|
let metadata = match fs::metadata(&safe_path).await {
|
||||||
Ok(m) => m,
|
Ok(m) => m,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Ошибка получения метаданных: {e}");
|
eprintln!("Error getting metadata: {e}");
|
||||||
return Err(StatusCode::NOT_FOUND); // Лучше NOT_FOUND для отсутствующих файлов
|
return Err(StatusCode::NOT_FOUND);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 1. Если это директория - рендерим индекс
|
|
||||||
if metadata.is_dir() {
|
if metadata.is_dir() {
|
||||||
return render_directory_index(&safe_path, &full_path)
|
return render_directory_index(&safe_path, &full_path)
|
||||||
.await
|
.await
|
||||||
.map(|h| h.into_response());
|
.map(|h| h.into_response());
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. ПРОВЕРКА НА ИЗОБРАЖЕНИЯ (И другую статику)
|
|
||||||
// Распространенные расширения изображений
|
|
||||||
let extension = safe_path
|
let extension = safe_path
|
||||||
.extension()
|
.extension()
|
||||||
.and_then(|ext| ext.to_str())
|
.and_then(|ext| ext.to_str())
|
||||||
@ -225,30 +217,24 @@ async fn serve_file(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if is_image {
|
if is_image {
|
||||||
// Читаем файл в байты
|
|
||||||
let file_content = match fs::read(&safe_path).await {
|
let file_content = match fs::read(&safe_path).await {
|
||||||
Ok(content) => content,
|
Ok(content) => content,
|
||||||
Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR),
|
Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Определяем MIME тип
|
|
||||||
let mime_type = from_path(&safe_path).first_or_octet_stream();
|
let mime_type = from_path(&safe_path).first_or_octet_stream();
|
||||||
|
|
||||||
// Возвращаем файл с правильным заголовком Content-Type
|
|
||||||
return Ok(([(header::CONTENT_TYPE, mime_type.as_ref())], file_content).into_response());
|
return Ok(([(header::CONTENT_TYPE, mime_type.as_ref())], file_content).into_response());
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Если это не картинка и не папка, считаем что это Markdown (или текст)
|
|
||||||
// Читаем контент как строку
|
|
||||||
let content = match fs::read_to_string(&safe_path).await {
|
let content = match fs::read_to_string(&safe_path).await {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Ошибка чтения: {e}");
|
eprintln!("Error reading: {e}");
|
||||||
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Логика кнопки "Назад"
|
|
||||||
let back_link = if let Some(pos) = full_path.rfind('/') {
|
let back_link = if let Some(pos) = full_path.rfind('/') {
|
||||||
let parent = &full_path[..pos];
|
let parent = &full_path[..pos];
|
||||||
if parent.is_empty() {
|
if parent.is_empty() {
|
||||||
@ -260,22 +246,13 @@ async fn serve_file(
|
|||||||
"/".to_string()
|
"/".to_string()
|
||||||
};
|
};
|
||||||
|
|
||||||
let back_button_html = format!(
|
|
||||||
r#"<div style="margin-bottom: 20px;">
|
|
||||||
<a href="{back_link}" style="display: inline-flex; align-items: center; color: #90a4ae; text-decoration: none; font-size: 0.95em; transition: color 0.2s;"
|
|
||||||
onmouseover="this.style.color='#ffffff'" onmouseout="this.style.color='#90a4ae'">
|
|
||||||
<span style="margin-right: 8px; font-size: 1.2em;">←</span> Назад
|
|
||||||
</a>
|
|
||||||
</div>"#
|
|
||||||
);
|
|
||||||
|
|
||||||
let html_content = markdown_to_html(&content, &state.syntax_set, &state.theme_set, &full_path);
|
let html_content = markdown_to_html(&content, &state.syntax_set, &state.theme_set, &full_path);
|
||||||
|
|
||||||
// Заполнение шаблона
|
// Заполнение шаблона
|
||||||
let final_html = TEMPLATE_FILE
|
let final_html = TEMPLATE_FILE
|
||||||
.replace("{{CONTENT}}", &html_content)
|
.replace("{{CONTENT}}", &html_content)
|
||||||
.replace("{{SSE_URL}}", &format!("/events/{full_path}"))
|
.replace("{{SSE_URL}}", &format!("/events/{full_path}"))
|
||||||
.replace("{{BACK_BUTTON}}", &back_button_html);
|
.replace("{{BACK_LINK}}", &back_link);
|
||||||
|
|
||||||
Ok(Html(final_html).into_response())
|
Ok(Html(final_html).into_response())
|
||||||
}
|
}
|
||||||
@ -287,7 +264,7 @@ async fn render_directory_index(
|
|||||||
let mut entries = match fs::read_dir(dir_path).await {
|
let mut entries = match fs::read_dir(dir_path).await {
|
||||||
Ok(list) => list,
|
Ok(list) => list,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Ошибка чтения директории: {e}");
|
eprintln!("Error directory reading: {e}");
|
||||||
return Err(StatusCode::FORBIDDEN);
|
return Err(StatusCode::FORBIDDEN);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -388,7 +365,7 @@ async fn run_file_watcher(state: AppState) {
|
|||||||
)
|
)
|
||||||
.expect("Failed to create watcher");
|
.expect("Failed to create watcher");
|
||||||
|
|
||||||
let watch_path = PathBuf::from(state.root);
|
let watch_path = state.root;
|
||||||
if let Err(e) = watcher.watch(&watch_path, RecursiveMode::Recursive) {
|
if let Err(e) = watcher.watch(&watch_path, RecursiveMode::Recursive) {
|
||||||
eprintln!("Ошибка настройки watcher: {e}");
|
eprintln!("Ошибка настройки watcher: {e}");
|
||||||
return;
|
return;
|
||||||
@ -401,120 +378,15 @@ async fn run_file_watcher(state: AppState) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
|
||||||
options.insert(Options::ENABLE_TABLES);
|
|
||||||
options.insert(Options::ENABLE_FOOTNOTES);
|
|
||||||
options.insert(Options::ENABLE_STRIKETHROUGH);
|
|
||||||
options.insert(Options::ENABLE_TASKLISTS);
|
|
||||||
options.insert(Options::ENABLE_SMART_PUNCTUATION);
|
|
||||||
|
|
||||||
let parser = pulldown_cmark::Parser::new_ext(markdown, options);
|
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
for event in parser {
|
|
||||||
match event {
|
|
||||||
Event::Start(Tag::CodeBlock(kind)) => {
|
|
||||||
in_code_block = true;
|
|
||||||
current_code.clear();
|
|
||||||
current_lang = if let CodeBlockKind::Fenced(l) = kind {
|
|
||||||
Some(l.to_string())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Event::End(Tag::CodeBlock(_)) => {
|
|
||||||
in_code_block = false;
|
|
||||||
let is_mermaid = current_lang.as_deref() == Some("mermaid");
|
|
||||||
|
|
||||||
if is_mermaid {
|
|
||||||
let escaped_code = escape_html(¤t_code);
|
|
||||||
let mermaid_html = format!(
|
|
||||||
r#"<div class="code-block-wrapper mermaid-wrapper">
|
|
||||||
<div class="code-header">
|
|
||||||
<span class="code-lang">Mermaid Diagram</span>
|
|
||||||
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
|
|
||||||
</div>
|
|
||||||
<div class="mermaid" style="background: transparent; padding: 20px; text-align: center;">{escaped_code}</div>
|
|
||||||
</div>"#
|
|
||||||
);
|
|
||||||
processed_events.push(Event::Html(mermaid_html.into()));
|
|
||||||
} else {
|
|
||||||
let lang_display = current_lang.as_deref().unwrap_or("text");
|
|
||||||
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() {
|
|
||||||
let line_with_newline = format!("{line}\n");
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
escape_html(¤t_code)
|
|
||||||
};
|
|
||||||
|
|
||||||
let code_container = format!(
|
|
||||||
r#"<div class="code-block-wrapper">
|
|
||||||
<div class="code-header">
|
|
||||||
<span class="code-lang">{lang_escaped}</span>
|
|
||||||
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
|
|
||||||
</div>
|
|
||||||
<pre style="margin: 0; border-radius: 0 0 6px 6px;"><code>{highlighted_html}</code></pre>
|
|
||||||
</div>"#
|
|
||||||
);
|
|
||||||
processed_events.push(Event::Html(code_container.into()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Event::Text(text) if in_code_block => {
|
|
||||||
current_code.push_str(&text);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
if !in_code_block {
|
|
||||||
processed_events.push(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut body_html = String::new();
|
|
||||||
html::push_html(&mut body_html, processed_events.into_iter());
|
|
||||||
body_html
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn random_file(State(state): State<AppState>) -> Result<impl IntoResponse, StatusCode> {
|
async fn random_file(State(state): State<AppState>) -> Result<impl IntoResponse, StatusCode> {
|
||||||
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()];
|
||||||
|
|
||||||
while let Some(current_dir) = stack.pop() {
|
while let Some(current_dir) = stack.pop() {
|
||||||
let mut entries = match fs::read_dir(¤t_dir).await {
|
let mut entries = match fs::read_dir(¤t_dir).await {
|
||||||
Ok(list) => list,
|
Ok(list) => list,
|
||||||
Err(_) => continue, // Пропускаем недоступные папки
|
Err(_) => continue,
|
||||||
};
|
};
|
||||||
|
|
||||||
while let Some(entry) = entries
|
while let Some(entry) = entries
|
||||||
@ -524,7 +396,6 @@ async fn random_file(State(state): State<AppState>) -> Result<impl IntoResponse,
|
|||||||
{
|
{
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
|
|
||||||
// Пропускаем скрытые файлы/папки
|
|
||||||
if entry.file_name().to_string_lossy().starts_with('.') {
|
if entry.file_name().to_string_lossy().starts_with('.') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -541,25 +412,14 @@ async fn random_file(State(state): State<AppState>) -> Result<impl IntoResponse,
|
|||||||
return Err(StatusCode::NOT_FOUND);
|
return Err(StatusCode::NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Выбираем случайный файл
|
|
||||||
let mut rng = rand::thread_rng();
|
let mut rng = rand::thread_rng();
|
||||||
let random_path = files.choose(&mut rng).unwrap();
|
let random_path = files.choose(&mut rng).unwrap();
|
||||||
|
|
||||||
// Получаем относительный путь от корня для формирования URL
|
|
||||||
let relative_path = random_path
|
let relative_path = random_path
|
||||||
.strip_prefix(&state.root)
|
.strip_prefix(&state.root)
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
|
||||||
let url_path = relative_path.to_string_lossy().replace('\\', "/"); // Нормализация для Windows
|
let url_path = relative_path.to_string_lossy().replace('\\', "/");
|
||||||
|
|
||||||
// Перенаправление (302 Found)
|
|
||||||
Ok(axum::response::Redirect::temporary(&format!("/{url_path}")))
|
Ok(axum::response::Redirect::temporary(&format!("/{url_path}")))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn escape_html(text: &str) -> String {
|
|
||||||
text.replace('&', "&")
|
|
||||||
.replace('<', "<")
|
|
||||||
.replace('>', ">")
|
|
||||||
.replace('"', """)
|
|
||||||
.replace('\'', "'")
|
|
||||||
}
|
|
||||||
|
|||||||
117
src/markdown.rs
Normal file
117
src/markdown.rs
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
use pulldown_cmark::{CodeBlockKind, Event, Options, Tag, html};
|
||||||
|
use syntect::easy::HighlightLines;
|
||||||
|
use syntect::highlighting::ThemeSet;
|
||||||
|
use syntect::html::{IncludeBackground, styled_line_to_highlighted_html};
|
||||||
|
use syntect::parsing::SyntaxSet;
|
||||||
|
|
||||||
|
pub 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();
|
||||||
|
options.insert(Options::ENABLE_TABLES);
|
||||||
|
options.insert(Options::ENABLE_FOOTNOTES);
|
||||||
|
options.insert(Options::ENABLE_STRIKETHROUGH);
|
||||||
|
options.insert(Options::ENABLE_TASKLISTS);
|
||||||
|
options.insert(Options::ENABLE_SMART_PUNCTUATION);
|
||||||
|
|
||||||
|
let parser = pulldown_cmark::Parser::new_ext(markdown, options);
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
for event in parser {
|
||||||
|
match event {
|
||||||
|
Event::Start(Tag::CodeBlock(kind)) => {
|
||||||
|
in_code_block = true;
|
||||||
|
current_code.clear();
|
||||||
|
current_lang = if let CodeBlockKind::Fenced(l) = kind {
|
||||||
|
Some(l.to_string())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Event::End(Tag::CodeBlock(_)) => {
|
||||||
|
in_code_block = false;
|
||||||
|
let is_mermaid = current_lang.as_deref() == Some("mermaid");
|
||||||
|
|
||||||
|
if is_mermaid {
|
||||||
|
let escaped_code = escape_html(¤t_code);
|
||||||
|
let mermaid_html = format!(
|
||||||
|
r#"<div class="code-block-wrapper mermaid-wrapper">
|
||||||
|
<div class="code-header">
|
||||||
|
<span class="code-lang">Mermaid Diagram</span>
|
||||||
|
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
|
||||||
|
</div>
|
||||||
|
<div class="mermaid" style="background: transparent; padding: 20px; text-align: center;">{escaped_code}</div>
|
||||||
|
</div>"#
|
||||||
|
);
|
||||||
|
processed_events.push(Event::Html(mermaid_html.into()));
|
||||||
|
} else {
|
||||||
|
let lang_display = current_lang.as_deref().unwrap_or("text");
|
||||||
|
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() {
|
||||||
|
let line_with_newline = format!("{line}\n");
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
escape_html(¤t_code)
|
||||||
|
};
|
||||||
|
|
||||||
|
let code_container = format!(
|
||||||
|
r#"<div class="code-block-wrapper">
|
||||||
|
<div class="code-header">
|
||||||
|
<span class="code-lang">{lang_escaped}</span>
|
||||||
|
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
|
||||||
|
</div>
|
||||||
|
<pre style="margin: 0; border-radius: 0 0 6px 6px;"><code>{highlighted_html}</code></pre>
|
||||||
|
</div>"#
|
||||||
|
);
|
||||||
|
processed_events.push(Event::Html(code_container.into()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::Text(text) if in_code_block => {
|
||||||
|
current_code.push_str(&text);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
if !in_code_block {
|
||||||
|
processed_events.push(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut body_html = String::new();
|
||||||
|
html::push_html(&mut body_html, processed_events.into_iter());
|
||||||
|
body_html
|
||||||
|
}
|
||||||
|
|
||||||
|
fn escape_html(text: &str) -> String {
|
||||||
|
text.replace('&', "&")
|
||||||
|
.replace('<', "<")
|
||||||
|
.replace('>', ">")
|
||||||
|
.replace('"', """)
|
||||||
|
.replace('\'', "'")
|
||||||
|
}
|
||||||
@ -46,7 +46,12 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
{{BACK_BUTTON}}
|
<div style="margin-bottom: 20px;">
|
||||||
|
<a href="{{BACK_LINK}}" style="display: inline-flex; align-items: center; color: #90a4ae; text-decoration: none; font-size: 0.95em; transition: color 0.2s;"
|
||||||
|
onmouseover="this.style.color='#ffffff'" onmouseout="this.style.color='#90a4ae'">
|
||||||
|
<span style="margin-right: 8px; font-size: 1.2em;">←</span> Назад
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
{{CONTENT}}
|
{{CONTENT}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user