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.
This commit is contained in:
parent
405e6340f8
commit
87237f5c28
3 changed files with 85 additions and 35 deletions
14
src/main.rs
14
src/main.rs
|
@ -87,8 +87,18 @@ fn main() {
|
||||||
|
|
||||||
info!("Compiling templates ...");
|
info!("Compiling templates ...");
|
||||||
let template_path = concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*");
|
let template_path = concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*");
|
||||||
let tera = compile_templates!(template_path);
|
let mut tera = compile_templates!(template_path);
|
||||||
let renderer = render::Renderer(tera);
|
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();
|
let renderer_addr: Addr<Syn, render::Renderer> = renderer.start();
|
||||||
|
|
||||||
info!("Initialising HTTP server ...");
|
info!("Initialising HTTP server ...");
|
||||||
|
|
|
@ -4,11 +4,17 @@
|
||||||
|
|
||||||
use actix::prelude::*;
|
use actix::prelude::*;
|
||||||
use actix_web::HttpResponse;
|
use actix_web::HttpResponse;
|
||||||
use tera::{Context, Tera};
|
|
||||||
use models::*;
|
|
||||||
use errors::*;
|
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 {
|
impl Actor for Renderer {
|
||||||
type Context = actix::Context<Self>;
|
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 {
|
fn handle(&mut self, msg: IndexPage, _: &mut Self::Context) -> Self::Result {
|
||||||
let mut ctx = Context::new();
|
let mut ctx = Context::new();
|
||||||
ctx.add("threads", &msg.threads);
|
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>;
|
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 {
|
impl Handler<ThreadPage> for Renderer {
|
||||||
type Result = Result<String>;
|
type Result = Result<String>;
|
||||||
|
|
||||||
fn handle(&mut self, msg: ThreadPage, _: &mut Self::Context) -> Self::Result {
|
fn handle(&mut self, msg: ThreadPage, _: &mut Self::Context) -> Self::Result {
|
||||||
|
let renderable = prepare_thread(&self.comrak, msg);
|
||||||
let mut ctx = Context::new();
|
let mut ctx = Context::new();
|
||||||
ctx.add("thread", &msg.thread);
|
ctx.add("title", &renderable.title);
|
||||||
ctx.add("posts", &msg.posts);
|
ctx.add("posts", &renderable.posts);
|
||||||
Ok(self.0.render("thread.html", &ctx)?)
|
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>;
|
type Result = Result<String>;
|
||||||
|
|
||||||
fn handle(&mut self, _: NewThreadPage, _: &mut Self::Context) -> Self::Result {
|
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())?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
<!-- Bootstrap CSS -->
|
<!-- 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">
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
|
@ -26,30 +26,10 @@
|
||||||
<div class="list-group-item flex-column">
|
<div class="list-group-item flex-column">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<h3>{{ thread.title }}</h3>
|
<h3>{{ title }}</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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 -%}
|
{% for post in posts -%}
|
||||||
<div class="list-group-item flex-column align-items-start">
|
<div class="list-group-item flex-column align-items-start">
|
||||||
|
@ -57,7 +37,7 @@
|
||||||
<div class="col-2 border-right">
|
<div class="col-2 border-right">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<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>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@ -69,15 +49,16 @@
|
||||||
<div class="col-10">
|
<div class="col-10">
|
||||||
{{ post.body }}
|
{{ post.body }}
|
||||||
</div>
|
</div>
|
||||||
<small class="text-muted"> {{ post.posted }} </small>
|
<small class="text-muted">{{ post.posted }}</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
|
|
||||||
<div class="list-group-item flex-column align-items-start">
|
<div class="list-group-item flex-column align-items-start">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<form action="/thread/reply" method="post">
|
<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>
|
<label for="body">You can use <strong>Markdown</strong>!</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<textarea class="form-control" id="body" name="body" aria-label="thread response"></textarea>
|
<textarea class="form-control" id="body" name="body" aria-label="thread response"></textarea>
|
||||||
|
|
Loading…
Reference in a new issue