Commit c179a4f809d04c0edab19d002f1fd13eb15f22b2

Authored by Georg Hopp
1 parent b9da2ed6

Add patch creation an apply on load logic

... ... @@ -8,7 +8,7 @@ PROFILE =
8 8 endif
9 9
10 10 start:
11   - systemfd --no-pid -s http::3000 -- \
  11 + systemfd --no-pid -s 0.0.0.0:3000 -- \
12 12 cargo watch -i static/ -s "make run"
13 13
14 14 wasm:
... ...
No preview for this file type
... ... @@ -6,71 +6,20 @@ mod models;
6 6 mod routes;
7 7 mod schema;
8 8
9   -use std::io::Write;
10   -use crate::routes::markdown::get_markdowns;
  9 +use crate::routes::markdown::{get_markdowns, update_markdown};
11 10 use crate::routes::other::*;
12 11 use crate::routes::user::*;
13 12
14 13 use actix_web::{guard, web, App, HttpResponse, HttpServer};
15 14 use diesel::r2d2::{self, ConnectionManager};
16 15 use diesel::SqliteConnection;
17   -use diffy::create_patch;
18   -use flate2::Compression;
19   -use flate2::write::{DeflateEncoder, DeflateDecoder};
20 16 use listenfd::ListenFd;
  17 +use routes::markdown::get_markdown;
21 18
22 19 pub(crate) type Pool = r2d2::Pool<ConnectionManager<SqliteConnection>>;
23 20
24   -const ORIGINAL :&str = r"This is a long Multiline text to
25   -see if we can create a small patch set from
26   -a longer text.
27   -
28   -This paragraph will be kept without modification. The reason I
29   -try this is to see if the diff becomes smaller than storing a
30   -compressed new version of this text...
31   -
32   -So this whole paragraph is obsolete. It will be removed in the
33   -modified text.
34   -
35   -There is probably a threshold up to what amount of text is needed
36   -to make the diff smaller.
37   -";
38   -
39   -const MODIFIED :&str = r"This is a long multiline text to
40   -see if we can create a small patch set from
41   -a longer text.
42   -
43   -This paragraph will be kept without modification. The reason I
44   -try this is to see if the diff becomes smaller than storing a
45   -compressed new version of this text...
46   -
47   -This is the replacement for the previous paragraph.
48   -
49   -There is probably a threshold up to what amount of text is needed
50   -to make the diff smaller.
51   -";
52   -
53 21 #[actix_rt::main]
54 22 async fn main() -> std::io::Result<()> {
55   - /* just some playing with diffy */
56   - let patch = format!("{}", create_patch(ORIGINAL, MODIFIED));
57   - println!("{} - {}", patch.len(), patch);
58   -
59   - let mut encoder = DeflateEncoder::new(Vec::new(), Compression::best());
60   - encoder.write_all(patch.as_bytes()).unwrap();
61   - let compressed = encoder.finish().unwrap();
62   - println!("{} - {:?}", compressed.len(), compressed);
63   -
64   - let mut decoder = DeflateDecoder::new(Vec::new());
65   - decoder.write_all(compressed.as_ref()).unwrap();
66   - let decompressed = decoder.finish().unwrap();
67   - let decompressed = match std::str::from_utf8(decompressed.as_ref()) {
68   - Ok(v) => v,
69   - Err(e) => panic!("Invalid UTF-8 sequence: {}", e),
70   - };
71   - println!("{} - {}", decompressed.len(), decompressed);
72   - /* ======= */
73   -
74 23 let mut listenfd = ListenFd::from_env();
75 24
76 25 dotenv::dotenv().ok();
... ... @@ -87,6 +36,10 @@ async fn main() -> std::io::Result<()> {
87 36 . service( web::resource("/markdowns")
88 37 . route(web::get().to(get_markdowns))
89 38 )
  39 + . service( web::resource("/markdowns/{id}")
  40 + . route(web::get().to(get_markdown))
  41 + . route(web::put().to(update_markdown))
  42 + )
90 43 . service( web::resource("/users")
91 44 . route(web::get().to(get_users))
92 45 . route(web::put().to(create_user))
... ...
... ... @@ -8,6 +8,10 @@ use diesel::{
8 8 RunQueryDsl
9 9 };
10 10 use serde::{Deserialize, Serialize};
  11 +use std::io::Write;
  12 +use diffy::{apply, create_patch, Patch};
  13 +use flate2::Compression;
  14 +use flate2::write::{DeflateEncoder, DeflateDecoder};
11 15
12 16
13 17 #[derive(Debug, Serialize, Deserialize, Queryable, Identifiable)]
... ... @@ -20,6 +24,16 @@ pub struct Markdown {
20 24 pub date_updated: String,
21 25 }
22 26
  27 +#[derive(Debug, Serialize, Deserialize, Queryable, Identifiable)]
  28 +#[table_name = "markdown_diffs"]
  29 +#[primary_key(markdown_id, diff_id)]
  30 +pub struct MarkdownDiff {
  31 + pub markdown_id: i32,
  32 + pub diff_id: i32,
  33 + pub diff: Vec<u8>,
  34 + pub date_created: String,
  35 +}
  36 +
23 37 #[derive(Debug, Insertable)]
24 38 #[table_name = "markdowns"]
25 39 pub struct MarkdownNew<'a> {
... ... @@ -30,12 +44,23 @@ pub struct MarkdownNew<'a> {
30 44 pub date_updated: &'a str,
31 45 }
32 46
  47 +#[derive(Debug, Insertable)]
  48 +#[table_name = "markdown_diffs"]
  49 +pub struct MarkdownDiffNew<'a> {
  50 + pub markdown_id: i32,
  51 + pub diff_id: i32,
  52 + pub diff: &'a [u8],
  53 + pub date_created: &'a str,
  54 +}
  55 +
33 56 #[derive(Debug, Serialize, Deserialize, AsChangeset)]
34 57 #[table_name="markdowns"]
35 58 pub struct MarkdownJson {
36 59 pub name: String,
37 60 pub content: String,
38 61 pub number_of_versions: i32,
  62 + pub date_created: String,
  63 + pub date_updated: String,
39 64 }
40 65
41 66 pub(crate) enum Action {
... ... @@ -82,11 +107,34 @@ pub(crate) fn get_markdowns(pool: Arc<Pool>) -> Result<Vec<Markdown>, Error>
82 107 }
83 108
84 109 pub(crate) fn get_markdown( pool: Arc<Pool>
85   - , ident: i32 ) -> Result<Markdown, Error>
  110 + , ident: &str
  111 + , patch: Option<i32> ) -> Result<Markdown, Error>
86 112 {
87 113 use crate::schema::markdowns::dsl::*;
  114 + use crate::schema::markdown_diffs::dsl::*;
88 115 let db_connection = pool.get()?;
89   - Ok(markdowns.find(ident).first::<Markdown>(&db_connection)?)
  116 +
  117 + let mut markdown = markdowns
  118 + . filter(name.eq(ident))
  119 + . first::<Markdown>(&db_connection)?;
  120 +
  121 + if let Some(patch) = patch {
  122 + let result = markdown_diffs . filter(markdown_id.eq(markdown.id))
  123 + . filter(diff_id.ge(patch))
  124 + . order(diff_id.desc())
  125 + . load::<MarkdownDiff>(&db_connection)?;
  126 +
  127 + let mut decoder = DeflateDecoder::new(Vec::new());
  128 + for patch in result {
  129 + decoder.write_all(patch.diff.as_ref()).unwrap();
  130 + let decomp = decoder.reset(Vec::new()).unwrap();
  131 + let decomp = Patch::from_str(
  132 + std::str::from_utf8(decomp.as_ref()).unwrap()).unwrap();
  133 + markdown.content = apply(&mut markdown.content, &decomp).unwrap();
  134 + }
  135 + };
  136 +
  137 + Ok(markdown)
90 138 }
91 139
92 140 pub(crate) fn delete_markdown( pool: Arc<Pool>
... ... @@ -98,19 +146,54 @@ pub(crate) fn delete_markdown( pool: Arc<Pool>
98 146 }
99 147
100 148 pub(crate) fn update_markdown( pool: Arc<Pool>
101   - , ident: i32
102   - , item: MarkdownJson ) -> Result<Markdown, Error>
  149 + , ident: String
  150 + , mut item: MarkdownJson ) -> Result<Markdown, Error>
103 151 {
104 152 use crate::schema::markdowns::dsl::*;
  153 + use crate::schema::markdown_diffs::dsl::*;
105 154 let db_connection = pool.get()?;
106   - let mut markdown = markdowns.find(ident).first::<Markdown>(&db_connection)?;
  155 + let mut markdown = markdowns
  156 + . filter(name.eq(ident))
  157 + . first::<Markdown>(&db_connection)?;
  158 +
  159 + let patch = format!( "{}", create_patch( item.content.as_str()
  160 + , markdown.content.as_str() ));
  161 + let mut encoder = DeflateEncoder::new(Vec::new(), Compression::best());
  162 + encoder.write_all(patch.as_bytes()).unwrap();
  163 + let compressed = encoder.finish().unwrap();
  164 +
  165 + let last_diff = match markdown_diffs . filter(markdown_id.eq(markdown.id))
  166 + . order(diff_id.desc())
  167 + . first::<MarkdownDiff>(&db_connection)
  168 + {
  169 + Ok(result) => (result.diff_id, Some(result)),
  170 + Err(_) => (0, None),
  171 + };
  172 +
107 173 let now = chrono::Local::now().naive_local();
  174 + let new_markdown_diff = MarkdownDiffNew {
  175 + markdown_id: markdown.id,
  176 + diff_id: last_diff.0 + 1,
  177 + diff: compressed.as_ref(),
  178 + date_created: markdown.date_updated.as_str(),
  179 + };
  180 +
  181 + item.date_updated = format!("{}", now);
  182 + item.number_of_versions = item.number_of_versions + 1;
  183 +
  184 + db_connection.transaction::<_, Error, _>(|| {
  185 + insert_into(markdown_diffs) . values(&new_markdown_diff)
  186 + . execute(&db_connection)?;
  187 +
  188 + update(&markdown).set(&item).execute(&db_connection)?;
  189 +
  190 + Ok(())
  191 + }).unwrap();
108 192
109   - update(markdowns.find(ident)).set(&item).execute(&db_connection)?;
110 193 markdown.name = item.name;
111 194 markdown.content = item.content;
112 195 markdown.number_of_versions = item.number_of_versions;
113   - markdown.date_updated = format!("{}", now);
  196 + markdown.date_updated = item.date_updated;
114 197
115 198 Ok(markdown)
116 199 }
... ...
... ... @@ -3,12 +3,50 @@ use crate::Pool;
3 3
4 4 use actix_web::{Error, HttpResponse, web};
5 5 use anyhow::Result;
  6 +use serde::Deserialize;
  7 +
  8 +#[derive(Debug, Deserialize)]
  9 +pub struct Patchset {
  10 + patch: Option<i32>,
  11 +}
6 12
7 13 pub async fn get_markdowns(pool: web::Data<Pool>)
8 14 -> Result<HttpResponse, Error>
9 15 {
10   - Ok(web::block(move || markdown::get_markdowns(pool.into_inner()))
  16 + Ok( web::block(move || markdown::get_markdowns(pool.into_inner()))
  17 + . await
  18 + . map(|markdowns| HttpResponse::Ok().json(markdowns))
  19 + . map_err(|_| HttpResponse::InternalServerError())?
  20 + )
  21 +}
  22 +
  23 +pub async fn get_markdown( pool: web::Data<Pool>
  24 + , name: web::Path<String>
  25 + , patch: web::Query<Patchset>
  26 + ) -> Result<HttpResponse, Error>
  27 +{
  28 + let pool = pool.into_inner();
  29 + let name = name.into_inner();
  30 + let patch = patch.into_inner();
  31 +
  32 + Ok( web::block(move || markdown::get_markdown(pool, name.as_str(), patch.patch))
  33 + . await
  34 + . map(|markdowns| HttpResponse::Ok().json(markdowns))
  35 + . map_err(|_| HttpResponse::InternalServerError())?
  36 + )
  37 +}
  38 +
  39 +pub async fn update_markdown( pool: web::Data<Pool>
  40 + , name: web::Path<String>
  41 + , item: web::Json<markdown::MarkdownJson> )
  42 + -> Result<HttpResponse, Error>
  43 +{
  44 + let pool = pool.into_inner();
  45 + let name = name.into_inner();
  46 + let item = item.into_inner();
  47 +
  48 + Ok(web::block(move || markdown::update_markdown(pool, name, item))
11 49 . await
12   - . map(|markdowns| HttpResponse::Ok().json(markdowns))
  50 + . map(|markdown| HttpResponse::Ok().json(markdown))
13 51 . map_err(|_| HttpResponse::InternalServerError())?)
14 52 }
... ...
... ... @@ -12,6 +12,7 @@ crate-type = ["cdylib", "rlib"]
12 12 default = ["console_error_panic_hook"]
13 13
14 14 [dependencies]
  15 +katex = { version = "0.4", default-features = false, features = ["wasm-js"] }
15 16 pulldown-cmark = "0.9"
16 17 console_log = "^0.1"
17 18 log = "^0.4"
... ... @@ -39,6 +40,7 @@ features = [
39 40 "Headers",
40 41 "HtmlElement",
41 42 "HtmlInputElement",
  43 + "MouseEvent",
42 44 "Node",
43 45 "Request",
44 46 "RequestInit",
... ...
... ... @@ -3,7 +3,7 @@ mod data;
3 3 use js_sys::JsString;
4 4 use log::Level;
5 5 use mogwai::prelude::*;
6   -use web_sys::{RequestInit, RequestMode, Request, Response};
  6 +use web_sys::{RequestInit, RequestMode, Request, Response, MouseEvent};
7 7 use serde::{Deserialize, Serialize};
8 8 use std::panic;
9 9 use wasm_bindgen::prelude::*;
... ... @@ -12,6 +12,7 @@ use wasm_bindgen::prelude::*;
12 12 enum AppLogic {
13 13 Update,
14 14 Toggle,
  15 + Store,
15 16 }
16 17
17 18 fn md_to_html(source: &str) -> String {
... ... @@ -24,21 +25,23 @@ fn md_to_html(source: &str) -> String {
24 25 html_output
25 26 }
26 27
27   -#[derive(Debug, Serialize, Deserialize)]
  28 +#[derive(Clone, Debug, Serialize, Deserialize)]
28 29 pub struct MarkdownJson {
29 30 pub name: String,
30 31 pub content: String,
31 32 pub number_of_versions: i32,
  33 + pub date_created: String,
  34 + pub date_updated: String,
32 35 }
33 36
34   -pub type MarkdownsJson = Vec<MarkdownJson>;
35   -
36   -async fn md_from_db() -> String {
  37 +async fn md_from_db() -> MarkdownJson {
37 38 let window = web_sys::window().unwrap();
38 39 let mut opts = RequestInit::new();
39 40 opts.method("GET").mode(RequestMode::Cors);
40 41
41   - let request = Request::new_with_str_and_init("/api/v0/markdowns", &opts).unwrap();
  42 + let request = Request::new_with_str_and_init(
  43 + "/api/v0/markdowns/md-example?patch=3"
  44 + , &opts ).unwrap();
42 45 request.headers().set("Accept", "application/json").unwrap();
43 46
44 47 let response = JsFuture::from(window.fetch_with_request(&request))
... ... @@ -46,15 +49,38 @@ async fn md_from_db() -> String {
46 49 . dyn_into::<Response>().unwrap();
47 50 let data = String::from( JsFuture::from(response.text().unwrap())
48 51 . await.unwrap()
49   - . dyn_into::<JsString>().unwrap() );
50   - let data :MarkdownsJson = serde_json::from_str(data.as_str()).unwrap();
  52 + . dyn_into::<JsString>().unwrap()
  53 + );
  54 + let data :MarkdownJson = serde_json::from_str(data.as_str()).unwrap();
  55 +
  56 + data.clone()
  57 +}
  58 +
  59 +async fn md_to_db(md :MarkdownJson) {
  60 + let encoded = serde_json::to_string(&md).unwrap();
  61 +
  62 + let window = web_sys::window().unwrap();
  63 + let mut opts = RequestInit::new();
  64 + opts . method("PUT")
  65 + . mode(RequestMode::Cors)
  66 + . body(Some(&encoded.into()));
51 67
52   - String::from(&data[0].content)
  68 + let url_str = format!("/api/v0/markdowns/{}", md.name);
  69 + let request = Request::new_with_str_and_init(url_str.as_str(), &opts)
  70 + . unwrap();
  71 + request.headers().set("Content-Type", "application/json").unwrap();
  72 +
  73 + let _response = JsFuture::from(window.fetch_with_request(&request))
  74 + . await.unwrap()
  75 + . dyn_into::<Response>().unwrap();
  76 +
  77 + /* do something with the response here.... */
53 78 }
54 79
55 80 async fn editor_logic( mut rx_logic: broadcast::Receiver<AppLogic>
56 81 , tx_view: broadcast::Sender<String>
57   - , mut rx_dom: broadcast::Receiver<Dom> ) {
  82 + , mut rx_dom: broadcast::Receiver<Dom>
  83 + , md :MarkdownJson ) {
58 84 let dom = rx_dom.next().await.unwrap();
59 85 let mut show_edit = false;
60 86
... ... @@ -82,8 +108,39 @@ async fn editor_logic( mut rx_logic: broadcast::Receiver<AppLogic>
82 108
83 109 update(&dom);
84 110
  111 + /* play with katex ==== */
  112 + let opts = katex::Opts::builder()
  113 + . output_type(katex::opts::OutputType::Mathml)
  114 + . build().unwrap();
  115 + let formula1 = katex::render_with_opts("E = mc^2", &opts).unwrap();
  116 + let formula2 = katex::render_with_opts("e^{i*\\pi} +1 = 0", &opts).unwrap();
  117 +
  118 + if let Either::Left(dom_js) = dom.inner_read() {
  119 + dom_js . to_owned()
  120 + . dyn_into::<Node>().unwrap()
  121 + . child_nodes().get(1).unwrap()
  122 + . child_nodes().get(2).unwrap()
  123 + . dyn_into::<HtmlElement>().unwrap()
  124 + . set_inner_html(formula1.as_str())
  125 + };
  126 +
  127 + if let Either::Left(dom_js) = dom.inner_read() {
  128 + dom_js . to_owned()
  129 + . dyn_into::<Node>().unwrap()
  130 + . child_nodes().get(1).unwrap()
  131 + . child_nodes().get(3).unwrap()
  132 + . dyn_into::<HtmlElement>().unwrap()
  133 + . set_inner_html(formula2.as_str())
  134 + };
  135 + /* =========== */
  136 +
85 137 while let Some(msg) = rx_logic.next().await {
86 138 match msg {
  139 + AppLogic::Store => {
  140 + let mut new_md = md.clone();
  141 + new_md.content = get_md(&dom);
  142 + md_to_db(new_md).await;
  143 + },
87 144 AppLogic::Update => update(&dom),
88 145 AppLogic::Toggle => {
89 146 show_edit = ! show_edit;
... ... @@ -101,23 +158,42 @@ async fn editor_logic( mut rx_logic: broadcast::Receiver<AppLogic>
101 158 fn editor_view( tx_logic: broadcast::Sender<AppLogic>
102 159 , rx_view: broadcast::Receiver<String>
103 160 , tx_dom: broadcast::Sender<Dom>
104   - , init_data: &str
  161 + , md: &MarkdownJson
105 162 ) -> ViewBuilder<Dom> {
106 163 let ns = "http://www.w3.org/2000/svg";
107 164
  165 + let input_filter = tx_logic
  166 + . sink()
  167 + . contra_map(|_| AppLogic::Update);
  168 + let store_filter = tx_logic
  169 + . sink()
  170 + . contra_filter_map(|e :DomEvent| {
  171 + if let Either::Left(e) = e.clone_inner() {
  172 + let e = e.dyn_into::<MouseEvent>().unwrap();
  173 + match e.alt_key() {
  174 + true => Some(AppLogic::Store),
  175 + false => None
  176 + }
  177 + } else {
  178 + None
  179 + }
  180 + });
  181 + let toggle_filter = tx_logic
  182 + . sink()
  183 + . contra_map(|_| AppLogic::Toggle);
  184 +
108 185 builder! {
109 186 <div class="input"
110 187 style:width="33%"
111   - on:input=tx_logic.sink().contra_map(|_| AppLogic::Update)
  188 + on:input=input_filter
112 189 capture:view=tx_dom.sink()>
113 190 <div contenteditable="true"
114 191 style:cursor="text"
115 192 style:display=("none", rx_view)>
116   - <pre>{init_data}</pre>
  193 + <pre on:click=store_filter>{md.content.clone()}</pre>
117 194 </div>
118 195 <div>
119   - <button on:click=tx_logic . sink()
120   - . contra_map(|_| AppLogic::Toggle)>
  196 + <button on:click=toggle_filter>
121 197 <svg version="1.1" id="Capa_1" xmlns=ns
122 198 x="0px" y="0px" viewBox="0 0 220.001 220.001"
123 199 style:width="1.5em" style:height="1.5em">
... ... @@ -131,6 +207,8 @@ fn editor_view( tx_logic: broadcast::Sender<AppLogic>
131 207 </svg>
132 208 </button>
133 209 <div></div>
  210 + <div></div>
  211 + <div></div>
134 212 </div>
135 213 </div>
136 214 }
... ... @@ -141,13 +219,13 @@ pub async fn main() -> Result<(), JsValue> {
141 219 panic::set_hook(Box::new(console_error_panic_hook::hook));
142 220 console_log::init_with_level(Level::Trace).unwrap();
143 221
144   - let data = md_from_db().await;
  222 + let md = md_from_db().await;
145 223
146 224 let (tx_dom, rx_dom) = broadcast::bounded(1);
147 225 let (tx_logic, rx_logic) = broadcast::bounded(1);
148 226 let (tx_view, rx_view) = broadcast::bounded(1);
149   - let comp = Component::from( editor_view(tx_logic, rx_view, tx_dom, data.as_str()) )
150   - . with_logic( editor_logic(rx_logic, tx_view, rx_dom) );
  227 + let comp = Component::from( editor_view(tx_logic, rx_view, tx_dom, &md) )
  228 + . with_logic( editor_logic(rx_logic, tx_view, rx_dom, md) );
151 229
152 230 let page = Component::from(builder! {{comp}});
153 231 page.build()?.run()
... ...
Please register or login to post a comment