about summary refs log tree commit diff
diff options
context:
space:
mode:
authorVincent Ambo <tazjin@gmail.com>2018-04-11T11·25+0200
committerVincent Ambo <tazjin@gmail.com>2018-04-11T11·25+0200
commit87237f5c28f177830808aeb4710f72d31f14c045 (patch)
tree0e1c0468daeaabd472f347304c19f0617a4591f2
parent405e6340f86b5806e865c570e3afc28a1416cf34 (diff)
feat(render): Implement Markdown thread rendering & Gravatar
Implements a new thread rendering pipeline which all posts and the
main thread body are first converted to a `RenderablePost` structure.

During the conversion to this structure, the post body is rendered as
Markdown and the author's email address is converted into the format
required by Gravatar.
-rw-r--r--src/main.rs14
-rw-r--r--src/render.rs75
-rw-r--r--templates/thread.html31
3 files changed, 85 insertions, 35 deletions
diff --git a/src/main.rs b/src/main.rs
index a3ad3643044d..eeab96e83ced 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -87,8 +87,18 @@ fn main() {
 
     info!("Compiling templates ...");
     let template_path = concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*");
-    let tera = compile_templates!(template_path);
-    let renderer = render::Renderer(tera);
+    let mut tera = compile_templates!(template_path);
+    tera.autoescape_on(vec![]);
+    let comrak = comrak::ComrakOptions{
+        github_pre_lang: true,
+        ext_strikethrough: true,
+        ext_table: true,
+        ext_autolink: true,
+        ext_tasklist: true,
+        ext_footnotes: true,
+        ..Default::default()
+    };
+    let renderer = render::Renderer{ tera, comrak };
     let renderer_addr: Addr<Syn, render::Renderer> = renderer.start();
 
     info!("Initialising HTTP server ...");
diff --git a/src/render.rs b/src/render.rs
index 8dfce219b215..fee897f281fb 100644
--- a/src/render.rs
+++ b/src/render.rs
@@ -4,11 +4,17 @@
 
 use actix::prelude::*;
 use actix_web::HttpResponse;
-use tera::{Context, Tera};
-use models::*;
 use errors::*;
+use md5;
+use models::*;
+use tera::{escape_html, Context, Tera};
+use chrono::prelude::{DateTime, Utc};
+use comrak::{ComrakOptions, markdown_to_html};
 
-pub struct Renderer(pub Tera);
+pub struct Renderer {
+    pub tera: Tera,
+    pub comrak: ComrakOptions,
+}
 
 impl Actor for Renderer {
     type Context = actix::Context<Self>;
@@ -29,7 +35,7 @@ impl Handler<IndexPage> for Renderer {
     fn handle(&mut self, msg: IndexPage, _: &mut Self::Context) -> Self::Result {
         let mut ctx = Context::new();
         ctx.add("threads", &msg.threads);
-        Ok(self.0.render("index.html", &ctx)?)
+        Ok(self.tera.render("index.html", &ctx)?)
     }
 }
 
@@ -43,14 +49,67 @@ impl Message for ThreadPage {
     type Result = Result<String>;
 }
 
+// "Renderable" structures with data transformations applied.
+#[derive(Debug, Serialize)]
+struct RenderablePost {
+    id: i32,
+    body: String,
+    posted: DateTime<Utc>,
+    author_name: String,
+    author_gravatar: String,
+}
+
+/// This structure represents the transformed thread data with
+/// Markdown rendering and other changes applied.
+#[derive(Debug, Serialize)]
+struct RenderableThreadPage {
+    id: i32,
+    title: String,
+    posts: Vec<RenderablePost>,
+}
+
+/// Helper function for computing Gravatar links.
+fn md5_hex(input: &[u8]) -> String {
+    format!("{:x}", md5::compute(input))
+}
+
+fn prepare_thread(comrak: &ComrakOptions, page: ThreadPage) -> RenderableThreadPage {
+    let mut posts = vec![RenderablePost {
+        // Always pin the ID of the first post.
+        id: 0,
+        body: markdown_to_html(&page.thread.body, comrak),
+        posted: page.thread.posted,
+        author_name: page.thread.author_name,
+        author_gravatar: md5_hex(page.thread.author_email.as_bytes()),
+    }];
+
+    for post in page.posts {
+        posts.push(RenderablePost {
+            id: post.id,
+            body: markdown_to_html(&post.body, comrak),
+            posted: post.posted,
+            author_name: post.author_name,
+            author_gravatar: md5_hex(post.author_email.as_bytes()),
+        });
+    }
+
+    RenderableThreadPage {
+        posts,
+        id: page.thread.id,
+        title: escape_html(&page.thread.title),
+    }
+}
+
 impl Handler<ThreadPage> for Renderer {
     type Result = Result<String>;
 
     fn handle(&mut self, msg: ThreadPage, _: &mut Self::Context) -> Self::Result {
+        let renderable = prepare_thread(&self.comrak, msg);
         let mut ctx = Context::new();
-        ctx.add("thread", &msg.thread);
-        ctx.add("posts", &msg.posts);
-        Ok(self.0.render("thread.html", &ctx)?)
+        ctx.add("title", &renderable.title);
+        ctx.add("posts", &renderable.posts);
+        ctx.add("id", &renderable.id);
+        Ok(self.tera.render("thread.html", &ctx)?)
     }
 }
 
@@ -65,6 +124,6 @@ impl Handler<NewThreadPage> for Renderer {
     type Result = Result<String>;
 
     fn handle(&mut self, _: NewThreadPage, _: &mut Self::Context) -> Self::Result {
-        Ok(self.0.render("new-thread.html", &Context::new())?)
+        Ok(self.tera.render("new-thread.html", &Context::new())?)
     }
 }
diff --git a/templates/thread.html b/templates/thread.html
index ee5a3a4271fb..bbe288e92f13 100644
--- a/templates/thread.html
+++ b/templates/thread.html
@@ -5,7 +5,7 @@
     <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
     <!-- Bootstrap CSS -->
     <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
-    <title>Converse: {{ thread.title }}</title>
+    <title>Converse: {{ title }}</title>
   </head>
   <body>
     <header>
@@ -26,30 +26,10 @@
             <div class="list-group-item flex-column">
               <div class="row">
                 <div class="col-12">
-                  <h3>{{ thread.title }}</h3>
+                  <h3>{{ title }}</h3>
                 </div>
               </div>
             </div>
-            <div class="list-group-item flex-column align-items-start">
-              <div class="row">
-                <div class="col-2 border-right">
-                  <div class="row">
-                    <div class="col-12">
-                      <img src="https://www.gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50" />
-                    </div>
-                  </div>
-                  <div class="row">
-                    <div class="col-12">
-                      <strong>{{ thread.author_name }}</strong>
-                    </div>
-                  </div>
-                </div>
-                <div class="col-10">
-                  {{ thread.body }}
-                </div>
-                <small class="text-muted"> {{ thread.posted }} </small>
-              </div>
-            </div>
 
             {% for post in posts -%}
             <div class="list-group-item flex-column align-items-start">
@@ -57,7 +37,7 @@
                 <div class="col-2 border-right">
                   <div class="row">
                     <div class="col-12">
-                      <img src="https://www.gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50" />
+                      <img src="https://www.gravatar.com/avatar/{{ post.author_gravatar }}?d=monsterid" />
                     </div>
                   </div>
                   <div class="row">
@@ -69,15 +49,16 @@
                 <div class="col-10">
                   {{ post.body }}
                 </div>
-                <small class="text-muted"> {{ post.posted }} </small>
+                <small class="text-muted">{{ post.posted }}</small>
               </div>
             </div>
             {%- endfor %}
+
             <div class="list-group-item flex-column align-items-start">
               <div class="row">
                 <div class="col-12">
                   <form action="/thread/reply" method="post">
-                    <input type="hidden" id="thread_id" name="thread_id" value="{{ thread.id }}">
+                    <input type="hidden" id="thread_id" name="thread_id" value="{{ id }}">
                     <label for="body">You can use <strong>Markdown</strong>!</label>
                     <div class="input-group">
                       <textarea class="form-control" id="body" name="body" aria-label="thread response"></textarea>