From 4f0c1e0f9c7ec8703305cc64f99812ff80e0e26f Mon Sep 17 00:00:00 2001 From: bitheaven Date: Sat, 13 Dec 2025 00:51:21 +0500 Subject: [PATCH] First commit --- .gitignore | 1 + Cargo.lock | 54 +++++++ Cargo.toml | 7 + public/index.md | 85 +++++++++++ public/style.css | 111 ++++++++++++++ public/test.md | 3 + src/main.rs | 390 +++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 651 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 public/index.md create mode 100644 public/style.css create mode 100644 public/test.md create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..84fa0ce --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,54 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "md_server_rs" +version = "0.1.0" +dependencies = [ + "regex", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..ca035b9 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "md_server_rs" +version = "0.1.0" +edition = "2024" + +[dependencies] +regex = "1.12.2" diff --git a/public/index.md b/public/index.md new file mode 100644 index 0000000..5e1deff --- /dev/null +++ b/public/index.md @@ -0,0 +1,85 @@ +# Демонстрация возможностей Markdown + +Это полный пример всех основных возможностей Markdown. + +## Заголовки + +### Третий уровень +#### Четвертый уровень +##### Пятый уровень +###### Шестой уровень + +--- + +## Форматирование текста + +Это **жирный текст** или __альтернативный жирный__. + +Это *курсивный текст* или _альтернативный курсив_. + +Это ~~зачеркнутый текст~~. + +Можно комбинировать: **жирный и *курсив* вместе**. + +--- + +## Списки + +### Маркированный список + +- Первый пункт +- Второй пункт +- Третий пункт + - Вложенность пока не поддерживается + +### Нумерованный список + +1. Первый пункт +2. Второй пункт +3. Третий пункт + +--- + +## Код + +Инлайн код: `const x = 42;` + +Блок кода: +``` +fn main() { + println!("Hello from Rust!"); +} +``` + +--- + +## Цитаты + +> Это цитата. +> Она может занимать несколько строк. + +> Другая цитата. + +--- + +## Ссылки и изображения + +Это [ссылка на хуй](https://natribu.org). + +Изображение: ![Логотип](https://www.google.com/logos/doodles/2025/seasonal-holidays-2025-6753651837110711-s.png) + +--- + +## Горизонтальные линии + +Линия выше создана с помощью `---` + +Также можно использовать `***` или `___` + +___ + +## Комбинированный пример + +> *"Да как нахуй удалить этот Амиго"* (c) Касперский + +~~Старая версия~~ → **Новая версия** diff --git a/public/style.css b/public/style.css new file mode 100644 index 0000000..42801fd --- /dev/null +++ b/public/style.css @@ -0,0 +1,111 @@ +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif; + max-width: 900px; + margin: 0 auto; + padding: 20px; + line-height: 1.6; + color: #333; +} +.toc { + background: #f5f5f5; + border: 1px solid #ddd; + border-radius: 5px; + padding: 15px 20px; + margin-bottom: 30px; +} +.toc h2 { + margin-top: 0; + font-size: 1.2em; +} +.toc ul { + list-style: none; + padding-left: 0; +} +.toc li { + margin: 5px 0; +} +.toc a { + color: #0066cc; + text-decoration: none; +} +.toc a:hover { + text-decoration: underline; +} +.toc-h1 { padding-left: 0; font-weight: bold; } +.toc-h2 { padding-left: 20px; } +.toc-h3 { padding-left: 40px; } +.toc-h4 { padding-left: 60px; } +.toc-h5 { padding-left: 80px; } +.toc-h6 { padding-left: 100px; } +h1, h2, h3, h4, h5, h6 { + margin-top: 1.5em; + margin-bottom: 0.5em; +} +h1 { border-bottom: 2px solid #ddd; padding-bottom: 0.3em; font-size: 2em; } +h2 { border-bottom: 1px solid #eee; padding-bottom: 0.3em; font-size: 1.5em; } +h3 { font-size: 1.25em; } +h4 { font-size: 1.1em; } +h5 { font-size: 1em; } +h6 { font-size: 0.9em; color: #666; } +pre { + background: #f6f8fa; + padding: 15px; + border-radius: 5px; + overflow-x: auto; + border: 1px solid #e1e4e8; +} +code { + font-family: 'Courier New', Consolas, monospace; + background: #f6f8fa; + padding: 2px 6px; + border-radius: 3px; + font-size: 0.9em; +} +pre code { + background: none; + padding: 0; +} +blockquote { + border-left: 4px solid #ddd; + padding-left: 20px; + margin: 20px 0; + color: #666; + font-style: italic; +} +hr { + border: none; + border-top: 2px solid #eee; + margin: 30px 0; +} +ul, ol { + padding-left: 30px; + margin: 15px 0; +} +li { + margin: 5px 0; +} +a { + color: #0066cc; + text-decoration: none; +} +a:hover { + text-decoration: underline; +} +img { + max-width: 100%; + height: auto; + border-radius: 5px; + margin: 10px 0; +} +del { + color: #999; +} +strong { + font-weight: 600; +} +em { + font-style: italic; +} +p { + margin: 15px 0; +} \ No newline at end of file diff --git a/public/test.md b/public/test.md new file mode 100644 index 0000000..3744f70 --- /dev/null +++ b/public/test.md @@ -0,0 +1,3 @@ +# TEST + +test file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..2dc857c --- /dev/null +++ b/src/main.rs @@ -0,0 +1,390 @@ +use std::fs; +use std::io::{BufRead, BufReader, Write}; +use std::net::{TcpListener, TcpStream}; +use std::path::Path; +use regex::Regex; + +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_string()).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(&format!("public/{}.md", 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(filename: &str) -> Result { + 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(""); + toc_list + } else { + String::new() + }; + + Ok(format!( + r#" + + + + {} + + + + {} + {} + + "#, + filename, css, toc_html, html + )) +} + +struct TocItem { + level: usize, + text: String, + anchor: String, +} + +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("
\n"); + } + continue; + } + if in_code_block { + html.push_str(&escape_html(line)); + html.push('\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("\n"), + Some(ListType::Ordered) => html.push_str("\n"), + 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_string(), anchor: anchor.clone() }); + html.push_str(&format!("
{}
\n", 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_string(), anchor: anchor.clone() }); + html.push_str(&format!("
{}
\n", 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_string(), anchor: anchor.clone() }); + html.push_str(&format!("

{}

\n", 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_string(), anchor: anchor.clone() }); + html.push_str(&format!("

{}

\n", 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_string(), anchor: anchor.clone() }); + html.push_str(&format!("

{}

\n", 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_string(), anchor: anchor.clone() }); + html.push_str(&format!("

{}

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

{}

\n", 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("\n"), + _ => {} + } + } + html.push_str("
    \n"); + in_list = Some(ListType::Unordered); + } + html.push_str(&format!("
  • {}
  • \n", 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("
\n"), + _ => {} + } + } + html.push_str("
    \n"); + in_list = Some(ListType::Ordered); + } + html.push_str(&format!("
  1. {}
  2. \n", process_inline(&line[3..]))); + } else if line.is_empty() { + if in_list.is_some() { + match in_list { + Some(ListType::Unordered) => html.push_str("\n"), + Some(ListType::Ordered) => html.push_str("
\n"), + None => {} + } + in_list = None; + } + if in_blockquote { + html.push_str("
\n"); + in_blockquote = false; + } + } else { + html.push_str(&format!("

{}

\n", process_inline(line))); + } + } + + if in_list.is_some() { + match in_list { + Some(ListType::Unordered) => html.push_str("\n"), + Some(ListType::Ordered) => html.push_str("\n"), + None => {} + } + } + if in_blockquote { + html.push_str("\n"); + } + + (html, toc) +} + +#[derive(PartialEq, Clone, Copy)] +enum ListType { + Unordered, + Ordered, +} + +fn process_inline(text: &str) -> String { + let mut result = text.to_string(); + + 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!("\"{}\"", 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!("{}", 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_string() +} + +fn escape_html(s: &str) -> String { + s.replace('&', "&") + .replace('*', "*") + .replace('_', "_") + .replace('<', "<") + .replace('>', ">") + .replace('"', """) + .replace('\'', "'") +}