commit 7c20cf6f8ff15852c42ea0a124c7072173e41c60 from: Murilo Ijanc date: Sun Apr 5 00:22:20 2026 UTC add embedded http server for local preview commit - bf6a94c965569ff8cd4e55c0aff269a484f946ed commit + 7c20cf6f8ff15852c42ea0a124c7072173e41c60 blob - e268781dc44180e52ee6c9426f3c3740f1bdf74b blob + 2d2064bd5a743e522fa883ea1ed71d33bb7ca18b --- src/main.rs +++ src/main.rs @@ -15,7 +15,13 @@ // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. // -use std::{env, fs, path::{Path, PathBuf}, collections::HashMap, sync::OnceLock}; +use std::{ + collections::HashMap, + env, fs, + net::{TcpListener, TcpStream}, + path::{Path, PathBuf}, + sync::OnceLock, +}; fn main() { LOG_ENABLED.set(env::var("KSSG_LOG").is_ok()).ok(); @@ -24,11 +30,101 @@ fn main() { match args.get(1).map(|s| s.as_str()) { Some("build") => build(), - _ => eprintln!("usage: ksgg build"), + Some("serve") => serve(), + _ => eprintln!("usage: ksgg "), } } ////////////////////////////////////////////////////////////////////////////// +// Log +////////////////////////////////////////////////////////////////////////////// + +static LOG_ENABLED: OnceLock = OnceLock::new(); + +macro_rules! log { + ($($arg:tt)*) => { + if *LOG_ENABLED.get().unwrap_or(&false) { + eprintln!("[{}] {}", now_utc(), format_args!($($arg)*)); + } + }; +} + +////////////////////////////////////////////////////////////////////////////// +// Server +////////////////////////////////////////////////////////////////////////////// + +fn content_type(path: &Path) -> &'static str { + match path.extension().and_then(|e| e.to_str()) { + Some("html") => "text/html; charset=utf-8", + Some("css") => "text/css", + _ => "application/octet-stream", + } +} + +fn serve() { + build(); + + let addr = "127.0.0.1:8080"; + let listener = TcpListener::bind(addr).expect("bind"); + println!("serving at http://{}", addr); + + for stream in listener.incoming() { + let stream = match stream { + Ok(s) => s, + Err(_) => continue, + }; + handle_request(stream); + } +} + +fn handle_request(mut stream: TcpStream) { + use std::io::{BufRead, BufReader, Write}; + + let reader = BufReader::new(&stream); + let request_line = match reader.lines().next() { + Some(Ok(line)) => line, + _ => return, + }; + + log!("{}", request_line); + + // "GET /path HTTP/1.1" + let path = request_line.split_whitespace().nth(1).unwrap_or("/"); + + let mut file_path = PathBuf::from("public"); + file_path.push(&path[1..]); + + if file_path.is_dir() { + file_path.push("index.html"); + } + if !file_path.exists() + && !file_path.to_str().map(|s| s.contains('.')).unwrap_or(false) + { + file_path.set_extension("html"); + } + + let (status, body, ctype) = if file_path.exists() { + let body = fs::read(&file_path).expect("read file"); + let ctype = content_type(&file_path); + ("200 OK", body, ctype) + } else { + let body = b"404".to_vec(); + ("404 Not Found", body, "text/plain") + }; + + let response = format!( + "HTTP/1.1 {}\r\nContent-Type: {}\r\n\ + Content-Length: {}\r\nConnection: close\r\n\r\n", + status, + ctype, + body.len() + ); + + let _ = stream.write_all(response.as_bytes()); + let _ = stream.write_all(&body); +} + +////////////////////////////////////////////////////////////////////////////// // DateTime ////////////////////////////////////////////////////////////////////////////// @@ -68,20 +164,6 @@ fn now_utc() -> String { } ////////////////////////////////////////////////////////////////////////////// -// Log -////////////////////////////////////////////////////////////////////////////// - -static LOG_ENABLED: OnceLock = OnceLock::new(); - -macro_rules! log { - ($($arg:tt)*) => { - if *LOG_ENABLED.get().unwrap_or(&false) { - eprintln!("[{}] {}", now_utc(), format_args!($($arg)*)); - } - }; -} - -////////////////////////////////////////////////////////////////////////////// // Frontmatter ////////////////////////////////////////////////////////////////////////////// @@ -105,8 +187,8 @@ fn parse_options() -> markdown::ParseOptions { } fn parse_frontmatter(content: &str) -> Metadata { - let tree = markdown::to_mdast(content, &parse_options()) - .expect("parse mdast"); + let tree = + markdown::to_mdast(content, &parse_options()).expect("parse mdast"); let mut map = HashMap::new(); if let markdown::mdast::Node::Root(root) = tree { @@ -167,7 +249,6 @@ fn copy_dir(src: &Path, dst: &Path) { } } - ////////////////////////////////////////////////////////////////////////////// // Build ////////////////////////////////////////////////////////////////////////////// @@ -195,8 +276,7 @@ fn build() { continue; } - let md = fs::read_to_string(file) - .expect("read content"); + let md = fs::read_to_string(file).expect("read content"); let meta = parse_frontmatter(&md); if meta.draft { @@ -204,16 +284,24 @@ fn build() { continue; } - let html_content = markdown::to_html_with_options(&md, &markdown::Options { - parse: parse_options(), - ..markdown::Options::default() - }).expect("markdown to html"); + let html_content = markdown::to_html_with_options( + &md, + &markdown::Options { + parse: parse_options(), + ..markdown::Options::default() + }, + ) + .expect("markdown to html"); let relative = file.strip_prefix(content_dir).expect("strip prefix"); - let out_path = Path::new("public").join(relative).with_extension("html"); + let out_path = + Path::new("public").join(relative).with_extension("html"); - posts.push(Post { meta, html_content, out_path }); - + posts.push(Post { + meta, + html_content, + out_path, + }); } let post_list = generate_post_list(&posts); @@ -225,7 +313,6 @@ fn build() { if !nav.is_empty() { content.push_str(&nav); } - println!("{}", posts[i].out_path.display()); let list = if posts[i].out_path == Path::new("public/index.html") { &post_list @@ -287,16 +374,18 @@ fn series_nav(posts: &[Post], current: usize) -> Strin if let Some(p) = prev { let href = format!("/{}", p.out_path.display()); - nav.push_str( - &format!("\u{2190} {}", href, p.meta.title) - ); + nav.push_str(&format!( + "\u{2190} {}", + href, p.meta.title + )); } if let Some(p) = next { let href = format!("/{}", p.out_path.display()); - nav.push_str( - &format!("{} \u{2192}", href, p.meta.title) - ); + nav.push_str(&format!( + "{} \u{2192}", + href, p.meta.title + )); } nav.push_str(""); @@ -310,19 +399,21 @@ fn generate_post_list(posts: &[Post]) -> String { if post.out_path == Path::new("public/index.html") { continue; } - items.push(( - &post.meta.date, - &post.meta.title, - &post.out_path, - )); + items.push((&post.meta.date, &post.meta.title, &post.out_path)); } items.sort_by(|a, b| b.0.cmp(a.0)); let mut html = String::from("
    \n"); for (date, title, path) in &items { - let href = format!("/{}", path.strip_prefix("public").unwrap_or(path).display()); - html.push_str(&format!("
  • - {}
  • \n", date, href, title)); + let href = format!( + "/{}", + path.strip_prefix("public").unwrap_or(path).display() + ); + html.push_str(&format!( + "
  • - {}
  • \n", + date, href, title + )); } html.push_str("
");