use std::fs; use std::io::{BufRead, BufReader, Write}; use std::net::{TcpListener, TcpStream}; use std::path::Path; use regex::Regex; #[derive(PartialEq, Clone, Copy)] enum ListType { Unordered, Ordered, } struct TocItem { level: usize, text: String, anchor: String, } fn main() { if !Path::new("public").exists() { fs::create_dir("public").expect("Не удалось создать папку public"); println!("Создана папка public/"); } if !Path::new("public/style.css").exists() { fs::write("public/style.css", "").expect("Не удалось создать style.css"); println!("Создан файл public/style.css"); } if !Path::new("public/index.md").exists() { fs::write("public/index.md", "").expect("Не удалось создать index.md"); println!("Создан файл public/index.md"); } let listener = TcpListener::bind("127.0.0.1:8080").unwrap(); println!("\nСервер запущен на http://127.0.0.1:8080"); println!("Markdown файлы читаются из папки public/\n"); for stream in listener.incoming() { let stream = stream.unwrap(); handle_connection(stream); } } fn handle_connection(mut stream: TcpStream) { let buf_reader = BufReader::new(&stream); let re = Regex::new(r"^GET\s+(\S+)\s+HTTP/\d\.\d$").unwrap(); let request_line = buf_reader.lines().next().unwrap().unwrap(); let binding = re.captures(&request_line).and_then(|caps| caps.get(1)).map(|m| m.as_str().to_owned()).unwrap_or(String::new()); let request_line = binding.trim(); let request_line = if request_line == "/" { "index" } else { &request_line[1..] }; let (status_line, html_content) = match read_markdown(request_line) { Ok(html) => ("HTTP/1.1 200 OK", html), Err(_) => ("HTTP/1.1 404 NOT FOUND", String::from("

404 - Страница не найдена

")), }; let response = format!( "{}\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: {}\r\n\r\n{}", status_line, html_content.len(), html_content ); stream.write_all(response.as_bytes()).unwrap(); stream.flush().unwrap(); } fn read_markdown(request_line: &str) -> Result { let filename = format!("public/{}.md", request_line); let markdown_content = fs::read_to_string(&filename)?; let (html, toc) = markdown_to_html(&markdown_content); let css = fs::read_to_string("public/style.css")?; let toc_html = if !toc.is_empty() { let mut toc_list = String::from(r#""); toc_list } else { String::new() }; let breadcrumbs = breadcrumbs(request_line); Ok(format!( r#" {} {} {} {} "#, filename, css, breadcrumbs, toc_html, html )) } fn breadcrumbs(request_line: &str) -> String { if request_line == "index" { return String::new() } let mut crumbs = vec![String::from(r#"/"#)]; let mut url = String::new(); for part in request_line.split("/") { url = format!("{}/{}", url, part); let crumb = String::from(format!(r#"{}"#, url, part)); crumbs.push(crumb); } let crumbs = crumbs.join(" > "); format!(r#""#, crumbs) } fn markdown_to_html(markdown: &str) -> (String, Vec) { let mut html = String::new(); let mut toc = Vec::new(); let mut in_code_block = false; let mut in_list: Option = None; let mut in_blockquote = false; for line in markdown.lines() { if line.starts_with("```") { in_code_block = !in_code_block; if in_code_block { html.push_str("
");
			} else {
				html.push_str("
"); } continue; } if in_code_block { html.push_str(&escape_html(line)); html.push_str("\n"); continue; } let is_list_item = line.starts_with("- ") || line.starts_with("* ") || (line.len() > 2 && line.chars().next().unwrap().is_numeric() && line.chars().nth(1) == Some('.')); if in_list.is_some() && !is_list_item && !line.is_empty() { match in_list { Some(ListType::Unordered) => html.push_str(""), Some(ListType::Ordered) => html.push_str(""), None => {} } in_list = None; } if in_blockquote && !line.starts_with("> ") && !line.is_empty() { html.push_str("\n"); in_blockquote = false; } if line.starts_with("###### ") { let text = &line[7..]; let anchor = create_anchor(text); toc.push(TocItem { level: 6, text: text.to_owned(), anchor: anchor.clone() }); html.push_str(&format!(r#"
{}
"#, anchor, process_inline(text))); } else if line.starts_with("##### ") { let text = &line[6..]; let anchor = create_anchor(text); toc.push(TocItem { level: 5, text: text.to_owned(), anchor: anchor.clone() }); html.push_str(&format!(r#"
{}
"#, anchor, process_inline(text))); } else if line.starts_with("#### ") { let text = &line[5..]; let anchor = create_anchor(text); toc.push(TocItem { level: 4, text: text.to_owned(), anchor: anchor.clone() }); html.push_str(&format!(r#"

{}

"#, anchor, process_inline(text))); } else if line.starts_with("### ") { let text = &line[4..]; let anchor = create_anchor(text); toc.push(TocItem { level: 3, text: text.to_owned(), anchor: anchor.clone() }); html.push_str(&format!(r#"

{}

"#, anchor, process_inline(text))); } else if line.starts_with("## ") { let text = &line[3..]; let anchor = create_anchor(text); toc.push(TocItem { level: 2, text: text.to_owned(), anchor: anchor.clone() }); html.push_str(&format!(r#"

{}

"#, anchor, process_inline(text))); } else if line.starts_with("# ") { let text = &line[2..]; let anchor = create_anchor(text); toc.push(TocItem { level: 1, text: text.to_owned(), anchor: anchor.clone() }); html.push_str(&format!(r#"

{}

"#, anchor, process_inline(text))); } else if line == "---" || line == "***" || line == "___" { html.push_str("
"); } else if line.starts_with("> ") { if !in_blockquote { html.push_str("
"); in_blockquote = true; } html.push_str(&format!("

{}

", process_inline(&line[2..]))); } else if line.starts_with("- ") || line.starts_with("* ") { if in_list != Some(ListType::Unordered) { if in_list.is_some() { match in_list { Some(ListType::Ordered) => html.push_str(""), _ => {} } } html.push_str("
    "); in_list = Some(ListType::Unordered); } html.push_str(&format!("
  • {}
  • ", process_inline(&line[2..]))); } else if line.len() > 2 && line.chars().next().unwrap().is_numeric() && line.chars().nth(1) == Some('.') { if in_list != Some(ListType::Ordered) { if in_list.is_some() { match in_list { Some(ListType::Unordered) => html.push_str("
"), _ => {} } } html.push_str("
    "); in_list = Some(ListType::Ordered); } html.push_str(&format!("
  1. {}
  2. ", process_inline(&line[3..]))); } else if line.is_empty() { if in_list.is_some() { match in_list { Some(ListType::Unordered) => html.push_str(""), Some(ListType::Ordered) => html.push_str("
"), None => {} } in_list = None; } if in_blockquote { html.push_str("
"); in_blockquote = false; } } else { html.push_str(&format!("

{}

", process_inline(line))); } } if in_list.is_some() { match in_list { Some(ListType::Unordered) => html.push_str(""), Some(ListType::Ordered) => html.push_str(""), None => {} } } if in_blockquote { html.push_str(""); } (html, toc) } fn process_inline(text: &str) -> String { let mut result = text.to_owned(); while let Some(start) = result.find('`') { if let Some(end) = result[start + 1..].find('`') { let inner = &result[start + 1..start + 1 + end]; let replaced = format!("{}", escape_html(inner)); result = format!("{}{}{}", &result[..start], replaced, &result[start + 2 + end..]); } else { break; } } while let Some(start) = result.find("**") { if let Some(end) = result[start + 2..].find("**") { let inner = &result[start + 2..start + 2 + end]; let replaced = format!("{}", inner); result = format!("{}{}{}", &result[..start], replaced, &result[start + 4 + end..]); } else { break; } } while let Some(start) = result.find("__") { if let Some(end) = result[start + 2..].find("__") { let inner = &result[start + 2..start + 2 + end]; let replaced = format!("{}", inner); result = format!("{}{}{}", &result[..start], replaced, &result[start + 4 + end..]); } else { break; } } let mut pos = 0; while let Some(start) = result[pos..].find('*') { let abs_start = pos + start; if abs_start > 0 && result.chars().nth(abs_start - 1) == Some('*') { pos = abs_start + 1; continue; } if abs_start + 1 < result.len() && result.chars().nth(abs_start + 1) == Some('*') { pos = abs_start + 1; continue; } if let Some(end) = result[abs_start + 1..].find('*') { let abs_end = abs_start + 1 + end; if abs_end + 1 < result.len() && result.chars().nth(abs_end + 1) == Some('*') { pos = abs_start + 1; continue; } let inner = &result[abs_start + 1..abs_end]; let replaced = format!("{}", inner); result = format!("{}{}{}", &result[..abs_start], replaced, &result[abs_end + 1..]); pos = abs_start + replaced.len(); } else { break; } } let mut pos = 0; while let Some(start) = result[pos..].find('_') { let abs_start = pos + start; if abs_start > 0 && result.chars().nth(abs_start - 1) == Some('_') { pos = abs_start + 1; continue; } if abs_start + 1 < result.len() && result.chars().nth(abs_start + 1) == Some('_') { pos = abs_start + 1; continue; } if let Some(end) = result[abs_start + 1..].find('_') { let abs_end = abs_start + 1 + end; if abs_end + 1 < result.len() && result.chars().nth(abs_end + 1) == Some('_') { pos = abs_start + 1; continue; } let inner = &result[abs_start + 1..abs_end]; let replaced = format!("{}", inner); result = format!("{}{}{}", &result[..abs_start], replaced, &result[abs_end + 1..]); pos = abs_start + replaced.len(); } else { break; } } while let Some(start) = result.find("![") { if let Some(mid) = result[start..].find("](") { if let Some(end) = result[start + mid..].find(')') { let alt = &result[start + 2..start + mid]; let url = &result[start + mid + 2..start + mid + end]; let replaced = format!(r#"{}"#, url, alt); result = format!("{}{}{}", &result[..start], replaced, &result[start + mid + end + 1..]); } else { break; } } else { break; } } while let Some(start) = result.find('[') { if let Some(mid) = result[start..].find("](") { if let Some(end) = result[start + mid..].find(')') { let text = &result[start + 1..start + mid]; let url = &result[start + mid + 2..start + mid + end]; let replaced = format!(r#"{}"#, url, text); result = format!("{}{}{}", &result[..start], replaced, &result[start + mid + end + 1..]); } else { break; } } else { break; } } while let Some(start) = result.find("~~") { if let Some(end) = result[start + 2..].find("~~") { let inner = &result[start + 2..start + 2 + end]; let replaced = format!("{}", inner); result = format!("{}{}{}", &result[..start], replaced, &result[start + 4 + end..]); } else { break; } } result } fn create_anchor(text: &str) -> String { text.to_lowercase() .chars() .map(|c| match c { 'а'..='я' | 'a'..='z' | '0'..='9' => c, ' ' | '-' | '_' => '-', _ => '_', }) .collect::() .trim_matches('-') .to_owned() } fn escape_html(s: &str) -> String { s.replace('&', "&") .replace('*', "*") .replace('_', "_") .replace('<', "<") .replace('>', ">") .replace('"', """) .replace('\'', "'") }