Commit Diff


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 <build|serve>"),
     }
 }
 
 //////////////////////////////////////////////////////////////////////////////
+// Log
+//////////////////////////////////////////////////////////////////////////////
+
+static LOG_ENABLED: OnceLock<bool> = 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<bool> = 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!("<a href=\"{}\">\u{2190} {}</a>", href, p.meta.title)
-        );
+        nav.push_str(&format!(
+            "<a href=\"{}\">\u{2190} {}</a>",
+            href, p.meta.title
+        ));
     }
 
     if let Some(p) = next {
         let href = format!("/{}", p.out_path.display());
-        nav.push_str(
-            &format!("<a href=\"{}\">{} \u{2192}</a>", href, p.meta.title)
-        );
+        nav.push_str(&format!(
+            "<a href=\"{}\">{} \u{2192}</a>",
+            href, p.meta.title
+        ));
     }
 
     nav.push_str("</nav>");
@@ -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("<ul class=\"post-list\">\n");
     for (date, title, path) in &items {
-        let href = format!("/{}", path.strip_prefix("public").unwrap_or(path).display());
-        html.push_str(&format!("<li><time>{}</time> - <a href=\"{}\">{}</a></li>\n", date, href, title));
+        let href = format!(
+            "/{}",
+            path.strip_prefix("public").unwrap_or(path).display()
+        );
+        html.push_str(&format!(
+            "<li><time>{}</time> - <a href=\"{}\">{}</a></li>\n",
+            date, href, title
+        ));
     }
 
     html.push_str("</ul>");