Browse Source

Write posts in Markdown

master
Dylan Baker 3 years ago
parent
commit
a60aae12ca

+ 21
- 0
Cargo.lock View File

@@ -951,6 +951,7 @@ dependencies = [
951 951
  "async-std",
952 952
  "chrono",
953 953
  "dotenv",
954
+ "pulldown-cmark",
954 955
  "serde 1.0.115",
955 956
  "serde_json",
956 957
  "tera",
@@ -1154,6 +1155,17 @@ dependencies = [
1154 1155
  "unicode-xid",
1155 1156
 ]
1156 1157
 
1158
+[[package]]
1159
+name = "pulldown-cmark"
1160
+version = "0.7.2"
1161
+source = "registry+https://github.com/rust-lang/crates.io-index"
1162
+checksum = "ca36dea94d187597e104a5c8e4b07576a8a45aa5db48a65e12940d3eb7461f55"
1163
+dependencies = [
1164
+ "bitflags",
1165
+ "memchr",
1166
+ "unicase",
1167
+]
1168
+
1157 1169
 [[package]]
1158 1170
 name = "quote"
1159 1171
 version = "1.0.7"
@@ -1766,6 +1778,15 @@ dependencies = [
1766 1778
  "unic-common",
1767 1779
 ]
1768 1780
 
1781
+[[package]]
1782
+name = "unicase"
1783
+version = "2.6.0"
1784
+source = "registry+https://github.com/rust-lang/crates.io-index"
1785
+checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
1786
+dependencies = [
1787
+ "version_check",
1788
+]
1789
+
1769 1790
 [[package]]
1770 1791
 name = "unicode-bidi"
1771 1792
 version = "0.3.4"

+ 1
- 0
microblog-lib/Cargo.toml View File

@@ -10,6 +10,7 @@ edition = "2018"
10 10
 async-std = { version = "1.6.0", features = ["attributes"] }
11 11
 chrono = { version = "0.4", features = ["serde", "rustc-serialize"] }
12 12
 dotenv = "0.15.0"
13
+pulldown-cmark = { version = "0.7", default-features = false }
13 14
 serde = "1.0.115"
14 15
 serde_json = "1.0.57"
15 16
 tera = "1.5.0"

+ 4
- 0
microblog-lib/src/lib.rs View File

@@ -5,6 +5,7 @@ mod fs;
5 5
 mod middleware;
6 6
 mod post;
7 7
 mod routes;
8
+mod util;
8 9
 
9 10
 use middleware::*;
10 11
 
@@ -41,6 +42,9 @@ pub async fn build_app() -> Result<tide::Server<State>, tide::Error> {
41 42
         .with(require_guest)
42 43
         .get(routes::login_page)
43 44
         .post(routes::login);
45
+    app.at("/preview")
46
+        .with(require_auth)
47
+        .post(routes::preview_post);
44 48
     app.at("/logout").with(require_auth).post(routes::logout);
45 49
 
46 50
     Ok(app)

+ 7
- 3
microblog-lib/src/post.rs View File

@@ -2,13 +2,14 @@ use serde::{Deserialize, Serialize};
2 2
 use serde_json;
3 3
 use std::io::{Error, ErrorKind};
4 4
 
5
-use crate::fs;
5
+use crate::{fs, util};
6 6
 
7 7
 #[derive(Debug, Serialize, Deserialize)]
8 8
 pub struct Post {
9 9
     pub id: String,
10 10
     pub title: String,
11 11
     pub body: String,
12
+    pub html: String,
12 13
     pub date: String,
13 14
 }
14 15
 
@@ -17,15 +18,18 @@ impl Post {
17 18
         fs::write_post_to_disk(self)
18 19
     }
19 20
 
21
+    pub fn generate_html(&mut self) -> String {
22
+        util::generate_html(&self.body)
23
+    }
24
+
20 25
     pub fn from_str(blob: &str) -> Result<Post, Error> {
21
-        let mut post: Post = match serde_json::from_str(blob) {
26
+        let post: Post = match serde_json::from_str(blob) {
22 27
             Ok(p) => Ok(p),
23 28
             Err(_) => Err(Error::new(
24 29
                 ErrorKind::Other,
25 30
                 format!("Error deserializing post"),
26 31
             )),
27 32
         }?;
28
-        post.body = post.body.replace("\n", "<br>");
29 33
         Ok(post)
30 34
     }
31 35
 }

+ 10
- 1
microblog-lib/src/routes.rs View File

@@ -8,7 +8,7 @@ use uuid::Uuid;
8 8
 
9 9
 use std::env;
10 10
 
11
-use crate::{fs, post::Post, State};
11
+use crate::{fs, post::Post, util, State};
12 12
 
13 13
 #[derive(Debug, Serialize, Deserialize)]
14 14
 struct User {
@@ -36,6 +36,7 @@ pub async fn edit_post(req: Request<State>) -> Result {
36 36
     let post_id = req.param("id")?;
37 37
     let mut post = fs::get_one_post(post_id).await?;
38 38
     post.body = post.body.replace("<br>", "\n");
39
+    post.html = post.generate_html();
39 40
     context.insert("post", &post);
40 41
     render_html_response("edit.html", &context, req)
41 42
 }
@@ -45,12 +46,14 @@ pub async fn create_post(mut req: Request<State>) -> Result {
45 46
     post.id = Uuid::new_v4().to_string();
46 47
     post.date = Local::now().date().naive_local().to_string();
47 48
     post.body = post.body.trim().to_owned();
49
+    post.html = post.generate_html();
48 50
     post.save().await?;
49 51
     Ok(Redirect::new("/").into())
50 52
 }
51 53
 
52 54
 pub async fn update_post(mut req: Request<State>) -> Result {
53 55
     let mut post: Post = req.body_form().await?;
56
+    post.html = post.generate_html();
54 57
     post.save().await?;
55 58
     req.session_mut()
56 59
         .insert("flash_success", String::from("Post updated successfully"))?;
@@ -65,6 +68,12 @@ pub async fn delete_post(mut req: Request<State>) -> Result {
65 68
     render_json_response(json!({ "success": "true" }))
66 69
 }
67 70
 
71
+pub async fn preview_post(mut req: Request<State>) -> Result {
72
+    let body: String = req.body_string().await?;
73
+    let html = util::generate_html(&body);
74
+    render_json_response(json!({ "body": html }))
75
+}
76
+
68 77
 pub async fn login_page(req: Request<State>) -> Result {
69 78
     render_html_response("login.html", &Context::new(), req)
70 79
 }

+ 9
- 0
microblog-lib/src/util.rs View File

@@ -0,0 +1,9 @@
1
+use pulldown_cmark::{html, Options, Parser};
2
+
3
+pub fn generate_html(s: &str) -> String {
4
+    let options = Options::all();
5
+    let parser = Parser::new_ext(s, options);
6
+    let mut html_output = String::new();
7
+    html::push_html(&mut html_output, parser);
8
+    html_output
9
+}

+ 16
- 8
static/script.js View File

@@ -49,19 +49,27 @@ if (previewButton) {
49 49
 
50 50
     const form = document.querySelector('#post-form');
51 51
     const textarea = form.querySelector('[name=body]');
52
-    const preview = document.createElement('div');
53
-    const content = document.createElement('div');
54
-    preview.id = 'preview';
52
+    const previewContainer = document.createElement('div');
53
+    const contentContainer = document.createElement('div');
54
+    const content = textarea.value.trim();
55
+    previewContainer.id = 'preview';
55 56
 
56
-    if (textarea.value.trim().length === 0) {
57
+    if (content.length === 0) {
57 58
       return;
58 59
     }
59 60
 
60 61
     const closeButton = createCloseButton();
61 62
 
62
-    content.innerHTML = textarea.value;
63
-    preview.append(closeButton);
64
-    preview.append(content);
65
-    form.append(preview);
63
+    fetch('/preview', {
64
+      method: 'POST',
65
+      body: content,
66
+    }).then((r) =>
67
+      r.json().then((r) => {
68
+        contentContainer.innerHTML = r.body;
69
+        previewContainer.append(closeButton);
70
+        previewContainer.append(contentContainer);
71
+        form.append(previewContainer);
72
+      }),
73
+    );
66 74
   });
67 75
 }

+ 4
- 0
static/style.css View File

@@ -170,6 +170,10 @@ a:hover {
170 170
   font-size: 18px;
171 171
 }
172 172
 
173
+#preview {
174
+  margin: 1em 0;
175
+}
176
+
173 177
 @media (max-width: 500px) {
174 178
   .form__button {
175 179
     width: 100%;

+ 2
- 1
templates/components.html View File

@@ -14,7 +14,7 @@
14 14
     </div>
15 15
   </h3>
16 16
   <div class="post__body">
17
-    {{ post.body | safe }}
17
+    {{ post.html | safe }}
18 18
   </div>
19 19
 </div>
20 20
 {% endmacro %} {% macro form(post=false, action) %}
@@ -29,6 +29,7 @@
29 29
     name="date"
30 30
     value="{% if post %}{{ post.date }}{% endif %}"
31 31
   />
32
+  <input type="hidden" name="html" />
32 33
   <div class="form__field">
33 34
     <label for="title" class="form__label">Title</label>
34 35
     <input

Loading…
Cancel
Save