First commit
This commit is contained in:
390
src/main.rs
Normal file
390
src/main.rs
Normal 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('&', "&")
|
||||
.replace('*', "*")
|
||||
.replace('_', "_")
|
||||
.replace('<', "<")
|
||||
.replace('>', ">")
|
||||
.replace('"', """)
|
||||
.replace('\'', "'")
|
||||
}
|
||||
Reference in New Issue
Block a user