413 lines
12 KiB
Rust
413 lines
12 KiB
Rust
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> > </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('&', "&")
|
||
.replace('*', "*")
|
||
.replace('_', "_")
|
||
.replace('<', "<")
|
||
.replace('>', ">")
|
||
.replace('"', """)
|
||
.replace('\'', "'")
|
||
}
|