First commit

This commit is contained in:
2025-12-13 00:51:21 +05:00
commit 4f0c1e0f9c
7 changed files with 651 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target

54
Cargo.lock generated Normal file
View File

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

7
Cargo.toml Normal file
View File

@@ -0,0 +1,7 @@
[package]
name = "md_server_rs"
version = "0.1.0"
edition = "2024"
[dependencies]
regex = "1.12.2"

85
public/index.md Normal file
View File

@@ -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) Касперский
~~Старая версия~~**Новая версия**

111
public/style.css Normal file
View File

@@ -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;
}

3
public/test.md Normal file
View File

@@ -0,0 +1,3 @@
# TEST
test file

390
src/main.rs Normal file
View File

@@ -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("<h1>404 - Страница не найдена</h1>")),
};
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<String, std::io::Error> {
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("<nav class=\"toc\"><h2>Содержание</h2><ul>");
for item in toc {
toc_list.push_str(&format!(
"<li class=\"toc-h{}\"><a href=\"#{}\">{}</a></li>",
item.level, item.anchor, item.text
));
}
toc_list.push_str("</ul></nav>");
toc_list
} else {
String::new()
};
Ok(format!(
r#"<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{}</title>
<style>{}</style>
</head>
<body>
{}
{}
</body>
</html>"#,
filename, css, toc_html, html
))
}
struct TocItem {
level: usize,
text: String,
anchor: String,
}
fn markdown_to_html(markdown: &str) -> (String, Vec<TocItem>) {
let mut html = String::new();
let mut toc = Vec::new();
let mut in_code_block = false;
let mut in_list: Option<ListType> = 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("<pre><code>");
} else {
html.push_str("</code></pre>\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("</ul>\n"),
Some(ListType::Ordered) => html.push_str("</ol>\n"),
None => {}
}
in_list = None;
}
if in_blockquote && !line.starts_with("> ") && !line.is_empty() {
html.push_str("</blockquote>\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!("<h6 id=\"{}\">{}</h6>\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!("<h5 id=\"{}\">{}</h5>\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!("<h4 id=\"{}\">{}</h4>\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!("<h3 id=\"{}\">{}</h3>\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!("<h2 id=\"{}\">{}</h2>\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!("<h1 id=\"{}\">{}</h1>\n", anchor, process_inline(text)));
} else if line == "---" || line == "***" || line == "___" {
html.push_str("<hr>\n");
} else if line.starts_with("> ") {
if !in_blockquote {
html.push_str("<blockquote>\n");
in_blockquote = true;
}
html.push_str(&format!("<p>{}</p>\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("</ol>\n"),
_ => {}
}
}
html.push_str("<ul>\n");
in_list = Some(ListType::Unordered);
}
html.push_str(&format!("<li>{}</li>\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("</ul>\n"),
_ => {}
}
}
html.push_str("<ol>\n");
in_list = Some(ListType::Ordered);
}
html.push_str(&format!("<li>{}</li>\n", process_inline(&line[3..])));
} else if line.is_empty() {
if in_list.is_some() {
match in_list {
Some(ListType::Unordered) => html.push_str("</ul>\n"),
Some(ListType::Ordered) => html.push_str("</ol>\n"),
None => {}
}
in_list = None;
}
if in_blockquote {
html.push_str("</blockquote>\n");
in_blockquote = false;
}
} else {
html.push_str(&format!("<p>{}</p>\n", process_inline(line)));
}
}
if in_list.is_some() {
match in_list {
Some(ListType::Unordered) => html.push_str("</ul>\n"),
Some(ListType::Ordered) => html.push_str("</ol>\n"),
None => {}
}
}
if in_blockquote {
html.push_str("</blockquote>\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!("<code>{}</code>", 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!("<strong>{}</strong>", 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!("<strong>{}</strong>", 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!("<em>{}</em>", 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!("<em>{}</em>", 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!("<img src=\"{}\" alt=\"{}\">", 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!("<a href=\"{}\">{}</a>", 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!("<del>{}</del>", 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::<String>()
.trim_matches('-')
.to_string()
}
fn escape_html(s: &str) -> String {
s.replace('&', "&amp;")
.replace('*', "&ast;")
.replace('_', "&lowbar;")
.replace('<', "&lt;")
.replace('>', "&gt;")
.replace('"', "&quot;")
.replace('\'', "&#39;")
}