Kaynağa Gözat

Resolve article with markdown parsing.

Alexandre Leblanc 3 yıl önce
ebeveyn
işleme
2262977810
6 değiştirilmiş dosya ile 162 ekleme ve 29 silme
  1. 1 1
      .gitignore
  2. 46 0
      Cargo.lock
  3. 1 0
      Cargo.toml
  4. 26 0
      src/articles_repository.rs
  5. 35 28
      src/main.rs
  6. 53 0
      src/theme.rs

+ 1 - 1
.gitignore

@@ -10,4 +10,4 @@
 
 # Generated by Cargo
 /target/
-/content/
+/www/

+ 46 - 0
Cargo.lock

@@ -19,6 +19,29 @@ dependencies = [
  "tokio-util",
 ]
 
+[[package]]
+name = "actix-files"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e04dcf7654254676d434b0285e2298d577ed4826f67f536e7a39bb0f64721164"
+dependencies = [
+ "actix-http",
+ "actix-service",
+ "actix-utils",
+ "actix-web",
+ "askama_escape",
+ "bitflags",
+ "bytes",
+ "derive_more",
+ "futures-core",
+ "http-range",
+ "log",
+ "mime",
+ "mime_guess",
+ "percent-encoding",
+ "pin-project-lite",
+]
+
 [[package]]
 name = "actix-http"
 version = "3.2.1"
@@ -222,6 +245,12 @@ dependencies = [
  "alloc-no-stdlib",
 ]
 
+[[package]]
+name = "askama_escape"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341"
+
 [[package]]
 name = "autocfg"
 version = "1.1.0"
@@ -536,6 +565,12 @@ dependencies = [
  "itoa",
 ]
 
+[[package]]
+name = "http-range"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573"
+
 [[package]]
 name = "httparse"
 version = "1.7.1"
@@ -651,6 +686,16 @@ version = "0.3.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
 
+[[package]]
+name = "mime_guess"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef"
+dependencies = [
+ "mime",
+ "unicase",
+]
+
 [[package]]
 name = "miniz_oxide"
 version = "0.5.3"
@@ -1128,6 +1173,7 @@ dependencies = [
 name = "vcs-blog"
 version = "0.1.0"
 dependencies = [
+ "actix-files",
  "actix-web",
  "chrono",
  "fern",

+ 1 - 0
Cargo.toml

@@ -7,6 +7,7 @@ description = "Blog engine running on version control system to store articles"
 
 [dependencies]
 actix-web = "4"
+actix-files = "0.6.1"
 log = "0.4"
 fern = "0.6"
 chrono = "0.4"

+ 26 - 0
src/articles_repository.rs

@@ -22,3 +22,29 @@ impl ArticlesRepository for FsRepository {
             .collect::<Vec<String>>()
     }
 }
+
+#[cfg(test)]
+mod tests_fs_repository {
+    use super::*;
+
+    #[test]
+    fn when_path_is_empty_should_return_nothing() {
+        let repo = FsRepository {};
+        let values = repo.get_directory_listing("");
+        let expected: Vec<String> = Vec::new();
+
+        assert_eq!(expected, values);
+    }
+
+    #[test]
+    fn when_path_is_valid_should_return_proper_listing() {
+        let repo = FsRepository {};
+        let values = repo.get_directory_listing("./content");
+        let expected = vec![
+            "./content\\some-category\\article.md".to_owned(),
+            "./content\\test.md".to_owned(),
+        ];
+
+        assert_eq!(expected, values);
+    }
+}

+ 35 - 28
src/main.rs

@@ -1,5 +1,6 @@
 mod articles_repository;
 mod config;
+mod theme;
 mod vcs;
 
 extern crate chrono;
@@ -13,19 +14,17 @@ use std::{env, fs, io};
 use articles_repository::{ArticlesRepository, FsRepository};
 use config::Configuration;
 use pulldown_cmark::{html, Options, Parser};
+use theme::Theme;
 
 #[get("")]
 async fn show_articles_list() -> impl Responder {
     let repo: FsRepository = FsRepository {};
 
     const CONTENT_FOLDER: &str = "./content";
-    let estimated_path: String = env::current_dir()
-        .unwrap_or_default()
-        .to_str()
-        .unwrap_or_default()
-        .to_string();
-
-    log::trace!("Estimated content repository: {estimated_path}/{CONTENT_FOLDER}");
+    let exe_dir = env::current_dir()
+        .and_then(|dir| io::Result::Ok(dir.to_str().unwrap_or("").to_string()))
+        .unwrap();
+    log::trace!("Estimated content repository: {exe_dir}/{CONTENT_FOLDER}");
 
     let paths = repo.get_directory_listing(&CONTENT_FOLDER).join("\n");
 
@@ -35,21 +34,22 @@ async fn show_articles_list() -> impl Responder {
 #[get("/{path:.+}")]
 async fn show_article(path: web::Path<(String,)>) -> impl Responder {
     let p = &path.0;
-    let file = fs::read_to_string(p.as_str());
-
-    match file {
-        Ok(content) => {
-            log::trace!("Opening article located {}", p.as_str());
-            let mut options = Options::empty();
-            options.insert(Options::ENABLE_STRIKETHROUGH);
-            let parser = Parser::new_ext(content.as_str(), options);
-            let mut html_output = String::new();
-            html::push_html(&mut html_output, parser);
-
-            HttpResponse::Ok().body(html_output)
-        }
-        Err(error) => {
-            log::error!("Article located at path {} was not found.", p.as_str());
+
+    let exe_dir = env::current_dir()
+        .and_then(|dir| io::Result::Ok(dir.to_str().unwrap_or("").to_string()))
+        .unwrap();
+
+    let theme_path = format!("{}/www/themes/default", exe_dir);
+    log::info!("Theme path: {}", theme_path);
+
+    let theme = Theme::new(theme_path);
+
+    let rendered = theme.render_as_page(&p);
+
+    match rendered {
+        Ok(content) => HttpResponse::Ok().body(content),
+        Err(e) => {
+            log::error!("Couldn't find article located at '{}'. Error: {}", p, e);
             HttpResponse::NotFound().finish()
         }
     }
@@ -73,16 +73,23 @@ async fn main() -> std::io::Result<()> {
                 message
             ))
         })
-        .level(log::LevelFilter::Info)
+        .level(log::LevelFilter::Trace)
         .chain(std::io::stdout())
         .apply();
 
     HttpServer::new(|| {
-        App::new().wrap(Logger::default()).service(
-            web::scope("/a")
-                .service(show_article)
-                .service(show_articles_list),
-        )
+        App::new()
+            .wrap(Logger::default())
+            .service(actix_files::Files::new("/css", "./www/themes/default/css"))
+            .service(actix_files::Files::new(
+                "/images",
+                "./www/themes/default/images",
+            ))
+            .service(
+                web::scope("/a")
+                    .service(show_article)
+                    .service(show_articles_list),
+            )
     })
     .bind((config.listen_addr, config.listen_port))?
     .run()

+ 53 - 0
src/theme.rs

@@ -0,0 +1,53 @@
+use pulldown_cmark::{html, Parser};
+use std::error::Error;
+use std::path::Path;
+
+pub struct Theme {
+    root_path: String,
+    parser_options: pulldown_cmark::Options,
+}
+
+impl Theme {
+    pub fn new(root_path: String) -> Self {
+        let mut parser_options = pulldown_cmark::Options::empty();
+        parser_options.insert(pulldown_cmark::Options::ENABLE_STRIKETHROUGH);
+
+        Theme {
+            root_path,
+            parser_options,
+        }
+    }
+
+    pub fn render_as_page(&self, path: &String) -> Result<String, String> {
+        log::trace!("Rendering file '{}'", path);
+
+        let path = format!("{}.md", path);
+        let file = std::fs::read_to_string(&path);
+
+        match file {
+            Ok(content) => {
+                let parser = Parser::new_ext(content.as_str(), self.parser_options);
+                let mut html_output = String::new();
+                html::push_html(&mut html_output, parser);
+
+                let mut buf = String::new();
+                buf.push_str(self.get_fragment("header").as_str());
+                buf.push_str(html_output.as_str());
+                buf.push_str(self.get_fragment("footer").as_str());
+
+                Ok(buf)
+            }
+            Err(error) => {
+                log::error!("File located at path {} was not found.", path);
+                Err(error.to_string())
+            }
+        }
+    }
+
+    // TODO: Cache
+    fn get_fragment(&self, fragment_name: &str) -> String {
+        let path = format!("{}/{}.html.tpl", self.root_path, fragment_name);
+        log::trace!("Loading fragment file '{}'", path);
+        std::fs::read_to_string(path).unwrap_or("".to_owned())
+    }
+}