Files
md-server-rs/src/main.rs
2025-12-13 01:29:22 +05:00

413 lines
12 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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("<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(request_line: &str) -> Result<String, std::io::Error> {
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#"<nav class="toc"><h2>Содержание</h2><ul>"#);
for item in toc {
toc_list.push_str(&format!(
r##"<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()
};
let breadcrumbs = breadcrumbs(request_line);
Ok(format!(
r#"<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{}</title>
<style>{}</style>
</head>
<body>
{}
{}
{}
</body>
</html>"#,
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#"<a href="/" class="crumb">/</a>"#)];
let mut url = String::new();
for part in request_line.split("/") {
url = format!("{}/{}", url, part);
let crumb = String::from(format!(r#"<a href="{}" class="crumb">{}</a>"#, url, part));
crumbs.push(crumb);
}
let crumbs = crumbs.join("<span>&nbsp;&gt;&nbsp;</span>");
format!(r#"<div class="breadcrumbs">{}</div>"#, crumbs)
}
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>");
}
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("</ul>"),
Some(ListType::Ordered) => html.push_str("</ol>"),
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_owned(), anchor: anchor.clone() });
html.push_str(&format!(r#"<h6 id="{}">{}</h6>"#, 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#"<h5 id="{}">{}</h5>"#, 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#"<h4 id="{}">{}</h4>"#, 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#"<h3 id="{}">{}</h3>"#, 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#"<h2 id="{}">{}</h2>"#, 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#"<h1 id="{}">{}</h1>"#, anchor, process_inline(text)));
} else if line == "---" || line == "***" || line == "___" {
html.push_str("<hr>");
} else if line.starts_with("> ") {
if !in_blockquote {
html.push_str("<blockquote>");
in_blockquote = true;
}
html.push_str(&format!("<p>{}</p>", 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>"),
_ => {}
}
}
html.push_str("<ul>");
in_list = Some(ListType::Unordered);
}
html.push_str(&format!("<li>{}</li>", 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>"),
_ => {}
}
}
html.push_str("<ol>");
in_list = Some(ListType::Ordered);
}
html.push_str(&format!("<li>{}</li>", process_inline(&line[3..])));
} else if line.is_empty() {
if in_list.is_some() {
match in_list {
Some(ListType::Unordered) => html.push_str("</ul>"),
Some(ListType::Ordered) => html.push_str("</ol>"),
None => {}
}
in_list = None;
}
if in_blockquote {
html.push_str("</blockquote>");
in_blockquote = false;
}
} else {
html.push_str(&format!("<p>{}</p>", process_inline(line)));
}
}
if in_list.is_some() {
match in_list {
Some(ListType::Unordered) => html.push_str("</ul>"),
Some(ListType::Ordered) => html.push_str("</ol>"),
None => {}
}
}
if in_blockquote {
html.push_str("</blockquote>");
}
(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!("<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!(r#"<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!(r#"<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_owned()
}
fn escape_html(s: &str) -> String {
s.replace('&', "&amp;")
.replace('*', "&ast;")
.replace('_', "&lowbar;")
.replace('<', "&lt;")
.replace('>', "&gt;")
.replace('"', "&quot;")
.replace('\'', "&#39;")
}