Commit 7ce1c3061f5680c2a3758ee746b2dff3dec438ec

Authored by Georg GH. Hopp
2 parents ebe8266b 8a73c413

Merge branch 'feature/markdown-content-storage' into 'master'

Feature/markdown content storage

The markdown storage is feature complete...

When I am a bit more experienced with mogwai the code should be cleaned up.

See merge request !1
1 [workspace] 1 [workspace]
2 -members = [ "ui", "server" ] 2 +members = [ "server", "ui", "mogwai-list" ]
3 3
4 [profile.release] 4 [profile.release]
5 lto = true 5 lto = true
@@ -8,7 +8,7 @@ PROFILE = @@ -8,7 +8,7 @@ PROFILE =
8 endif 8 endif
9 9
10 start: 10 start:
11 - systemfd --no-pid -s http::3000 -- \ 11 + systemfd --no-pid -s 0.0.0.0:3000 -- \
12 cargo watch -i static/ -s "make run" 12 cargo watch -i static/ -s "make run"
13 13
14 wasm: 14 wasm:
  1 +# This is from the webpage:
  2 +# https://www.developer.com/languages/creating-an-api-with-rust-and-sqlite/
  3 +# I have fixed some things manually as the code on that page did not compile
  4 +# without.
  5 +
  6 +# Additional informations at https://actix.rs/docs/databases/ assume one
  7 +# should use web::block to access the database...
  8 +# TODO check what is the difference to this approach.
  9 +# - well, we use web::block already for write actions.
  10 +
  11 +# Introduction to rust async:
  12 +# https://gruberbastian.com/posts/rust_async/
  13 +# https://blog.logrocket.com/a-practical-guide-to-async-in-rust/
  14 +# https://os.phil-opp.com/async-await/
  15 +
  16 +# Simple explanation on technical terms synchronous, asynchronous, concurrent
  17 +# and parallel.
  18 +# https://medium.com/plain-and-simple/synchronous-vs-asynchronous-vs-concurrent-vs-parallel-4342bfb8b9f2
  19 +
  20 +[package]
  21 +name = "artshop-common"
  22 +version = "0.1.0"
  23 +workspace = ".."
  24 +edition = "2018"
  25 +
  26 +[dependencies]
  27 +serde = "^1.0"
  1 +pub mod types;
  1 +use serde::{Deserialize, Serialize};
  2 +
  3 +#[derive(Debug, Clone, Deserialize, Serialize)]
  4 +pub enum Either<L, R> {
  5 + Left(L),
  6 + Right(R)
  7 +}
  8 +
  9 +#[derive(Clone, Debug, Serialize, Deserialize)]
  10 +pub struct MarkdownJson {
  11 + pub name: String,
  12 + pub content: String,
  13 + pub number_of_versions: i32,
  14 + pub date_created: String,
  15 + pub date_updated: String,
  16 +}
  17 +
  18 +#[derive(Clone, Debug, Serialize, Deserialize)]
  19 +pub struct MarkdownDiffJson {
  20 + pub id: i32,
  21 + pub date_created: String,
  22 +}
No preview for this file type
@@ -2,4 +2,4 @@ @@ -2,4 +2,4 @@
2 # see diesel.rs/guides/configuring-diesel-cli 2 # see diesel.rs/guides/configuring-diesel-cli
3 3
4 [print_schema] 4 [print_schema]
5 -file = "src/schema.rs" 5 +file = "server/src/schema.rs"
1 -# Ein sehr schöner Titel 1 +# Markdown Cheatsheat
2 2
3 -## Ein sinnloser Text  
4 -  
5 -Hier kommt ganz viel Text der irgendwie auch was machen soll, aber Zeilen  
6 -sollen auch im &lt;pre&gt; Eingabefeld automatisch umbrechen.  
7 -  
8 -Ein neuner Paragraph beginnt nach einer Leerzeile.  
9 -Ein Umbruch entsteht wie gewohnt durch 2 spaces am Ende einer  
10 -Zeile.  
11 -  
12 -## Fußnoten 3 +## Überschriften
  4 +---
13 5
14 -Vllt. kann man sogar so was wie Fussnoten[^1] in den Markdown Text  
15 -einbinden... diese kann man dann irgendwo einbauen... 6 +# # &lt;H1&gt;
  7 +## ## &lt;H2&gt;
  8 +### ### &lt;H3&gt;
  9 +#### #### &lt;H4&gt;
  10 +##### ##### &lt;H5&gt;
  11 +###### ###### &lt;H6&gt;
16 12
  13 +## Absätze und Umbrüche
17 --- 14 ---
18 15
19 -[^1]: Zum Beispiel so...  
20 -  
21 -[^2]: Oder so... 16 +Ein einfacher Zeilenumbuch
  17 +verändert den Textfluß nicht. Spaces haben auch keinen Einfluß auf den Textfluß. Es ist selten eine gute Idee große Abstände innerhalb eines Textes zu habe, sollte man diese aber wirklich brauchen kann man auf &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; inline html &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; zurückgreifen.
22 18
23 -## inline html ist im Moment auch ok. 19 +Leerzeilen erzeugen neue Paragraphen. Um im formatierten Text einen Zeilenumbruch zu erzeugen verwendet man zwei Spaces vor einem Zeilenumbruch im Eingabetext.
  20 +Dies führt nicht zu einem Paragraphen.
24 21
25 -<pre>Lustigerweise geht auch inline html</pre> 22 +## Hervorhebungen
  23 +---
26 24
27 -## Listen for fun 25 +- *kursive (schwache) Hervorhebung*
  26 +- **fette (starke) Hervorhebung**
  27 +- ***kursiv und fette (sehr starke) Hervorhebung***
  28 +- *schwache mit **eingebetteter starker** Hervorhebung*
  29 +- **starke mit *eingebetteter schwacher* Hervorhebung**
  30 +- ~~durchgestrichen~~
  31 +- <u>untersteichen nur mit HTML</u>
  32 +- ~~*durchgestrichen kursiv*~~
  33 +- **~~fett durchgestrichen~~**
  34 +- *<u>kursiv unterstrichen</u>*
  35 +- <u>**unterstrichen fett**</u>
  36 +
  37 +## Listen
  38 +---
28 39
29 -- ein Liste  
30 - - mehr Liste  
31 - - diesmal als Subliste.  
32 -- und was auch immer...  
33 - 1. und nun Verschachtelt.  
34 - 1. Numeriert.  
35 - 2. huhuhu  
36 - 3. wie bitte.  
37 - 2. juhu  
38 -- noch mehr Liste 40 +- erster Listeneintrag
  41 + - erster Unterlisteneintrag
  42 + - zweiter Unterlisteneintrag
  43 +- zweiter Listeneintrag
  44 + 1. erster numerierter Listeneintrag
  45 + 1. erster numerierter Unterlisteneintrag
  46 + 2. zeiter numerierter Unterlisteneintrag
  47 + 3. dritter numerierter Unterlisteneintrag
  48 + 2. zweiter numerierter Listeneintrag
  49 +- dritter Listeneintrag
  50 + - [x] erster Auswahllisteneintrag
  51 + - [ ] zweiter Auswahllisteneintrag
  52 + - [ ] dritter Auswahllisteneintrag
  53 +- vierter Listeneintrag
  54 + 1. [ ] erster numerierter Auswahllisteneintrag
  55 + 2. [x] zweiter numerierter Auswahllisteneintrag
  56 +
  57 +## Code Blöcke
  58 +---
39 59
40 -## Preformated Text 60 + Dies ist ein codeblock durch Einrückung.
  61 + In diesem werden keine Formatierungen
  62 + vorgenommen.
41 63
42 -```Hier kommt der code``` 64 +Mit backticks lassen sich Codeblöcke mit Sprachinformation
  65 +erstellen. Theoretisch koennte fuer solche Böcke dann Syntax-Highliting eingebaut werden.
43 66
44 -Und hier der Paragraph mit `inline code` der auch sehr schön aussehen kann. 67 +```shell
  68 +#!/bin/env sh
45 69
46 -## Hervorhebungen 70 +FOO="foo"
47 71
48 -Man kann Text auch sehr schön formatieren. So ist es z.B. möglich  
49 -*Worte kursiv zu stellen* oder man kann **sie auch fett schreiben**.  
50 -Als spezielles feature kann der von mir verwendete Parser auch  
51 -~~Texte durchstreichen~~. 72 +function func() {
  73 + local BAR=bar
  74 +}
  75 +```
52 76
53 -Nur wenn man Text <u>unterstreichen</u> will muss man auf inline html  
54 -zurückgreifen. 77 +Auch in den Fließtext lassen sich `inline code` Elemente einfügen um z.B. einzelne Kommandos hervorzuheben.
55 78
56 -## Blockquotes und horizontale Linie 79 +## Zitate und horizontale Linie
  80 +---
57 81
58 -> Dies sollte jetzt als quote erkennbar sein. 82 +> Dies ist ein Zitat.
59 > 83 >
60 ->> Auch diese sind schachtelbar 84 +>> Zitate können verschachtelt sein.
61 > 85 >
62 -> Und weiter gehts. 86 +> Wir können also zitieren was jemand zitiert hat.
  87 +> Solange die Zeilen ohne Unterbrechung mit einem &gt;
  88 +> beginnen bleibt es ein Zitat
63 89
64 ----  
65 90
66 -> Aber dies ist ein neuer quote. 91 +> Sobald eine Zeile on führendes &gt; auftaucht endet ein
  92 +> Zitat.
67 93
68 ## Links 94 ## Links
  95 +---
69 96
70 Ein link kann inline geschrieben werden, so wie diese zu 97 Ein link kann inline geschrieben werden, so wie diese zu
71 [Heise.de](https://heise.de/ 'Heise.de') oder als Referenz am Ende des Textes 98 [Heise.de](https://heise.de/ 'Heise.de') oder als Referenz am Ende des Textes
72 wie diese nach [Telepolis][lnk1]. 99 wie diese nach [Telepolis][lnk1].
73 100
74 -## Bilder koennte man auch einbinden. 101 +## Bilder
  102 +---
75 103
76 Wie Links lassen sich auch Bilder wie mein 104 Wie Links lassen sich auch Bilder wie mein
77 ![Gravatar](https://www.gravatar.com/avatar/fd016c954ec4ed3a4315eeed6c8b97b8) 105 ![Gravatar](https://www.gravatar.com/avatar/fd016c954ec4ed3a4315eeed6c8b97b8)
@@ -85,7 +113,8 @@ Paragraphen zu plazieren. @@ -85,7 +113,8 @@ Paragraphen zu plazieren.
85 113
86 Etwas so wie hier. 114 Etwas so wie hier.
87 115
88 -## Tabellen sollten auch gehen... 116 +## Tabellen
  117 +---
89 118
90 Die folgenden Beispiele kommen von [markdown.land][lnk2]: 119 Die folgenden Beispiele kommen von [markdown.land][lnk2]:
91 120
@@ -103,12 +132,31 @@ Bananas | 1.89 | 6 @@ -103,12 +132,31 @@ Bananas | 1.89 | 6
103 132
104 und die Spaltenausrichtung kann man auch einstellen: 133 und die Spaltenausrichtung kann man auch einstellen:
105 134
106 -  
107 | Item | Price | # In stock | 135 | Item | Price | # In stock |
108 |--------------|:-----:|-----------:| 136 |--------------|:-----:|-----------:|
109 | Juicy Apples | 1.99 | 739 | 137 | Juicy Apples | 1.99 | 739 |
110 | Bananas | 1.8900 | 6 | 138 | Bananas | 1.8900 | 6 |
111 139
  140 +## Fußnoten
  141 +---
  142 +
  143 +Man kann auch verlinkte Fußnoten[^1] in den Text
  144 +einbinden. Die Fußnote selber kann dann an beliebige stelle im Text stehen.
  145 +
  146 +---
  147 +[^1]: Zum Beispiel so.
  148 +
  149 +[^2]: Diese Fußnote hat keine Verlinkung im Text.
  150 +
  151 +## HTML einbetten.
  152 +---
  153 +
  154 +<ul>
  155 +<li>
  156 +<pre>Man kann auch direkt HTML tags einbetten,
  157 +wie hier.</pre>
  158 +</li>
  159 +</ul>
112 160
113 [lnk1]: https://heise.de/tp/ 'Telepolis' 161 [lnk1]: https://heise.de/tp/ 'Telepolis'
114 [lnk2]: https://markdown.land/markdown-table 'markdown.land' 162 [lnk2]: https://markdown.land/markdown-table 'markdown.land'
  1 +# Markdown Historie.
  2 +
  3 +Jedes gespeicherte Markdown erhaelt eine historie ueber die jede vergangene
  4 +Version eingesehen werden kann. Gespeichert ist diese in Form eines
  5 +komprimierten diffs.
  6 +
  7 +Die Frontend Komponente kann jede beliebige existierende Patchversion
  8 +darstellen. Auf der Datenseite ist aber kein **undo** möglich. Genau wie man
  9 +in der Realität nicht in der Zeit zurück gehen kann kann man das auch nicht
  10 +mit den Inhalten. Würde man das zulassen, so würde entweder ein Teil der
  11 +Historie des Markdowns wegfallen müssen oder aber es würde ein beliebig
  12 +komplexer Baum von Histoien entstehen.
  13 +
  14 +Statt also ein **undo** auf eine frühere Version zuzulassen kann man in der
  15 +aktuellen Version auf frühere Stände zurück gehen und diese gegebenenfalls
  16 +noch anpassen. Dieser Stand wird wieder ein neuer Patch gegen die gegenwärtige
  17 +Version.
@@ -15,3 +15,10 @@ @@ -15,3 +15,10 @@
15 ## Free Artwork 15 ## Free Artwork
16 - [Designlooter](https://designlooter.com/ 'Designlooter') 16 - [Designlooter](https://designlooter.com/ 'Designlooter')
17 - [SVGRepo](https://www.svgrepo.com/ 'SVGRepo') 17 - [SVGRepo](https://www.svgrepo.com/ 'SVGRepo')
  18 +
  19 +## Send and async and using non Send types...
  20 +- [Rust Send Story](https://procmarco.netlify.app/blog/2021-05-04-a-story-about-async-rust-and-using-send-types/ 'Rust Send story')
  21 +
  22 +## Restful API
  23 +[ReadTheDocs apiguide](https://apiguide.readthedocs.io/en/latest/build_and_publish/use_RESTful_urls.html 'ReadTheDocs apiguide')
  24 +[RestfulApi](https://restfulapi.net/versioning/ 'RestfulApi')
  1 +-- This file should undo anything in `up.sql`
  2 +DROP INDEX "markdown_diffs_id";
  3 +DROP TABLE "markdown_diffs";
  4 +DROP TABLE "markdowns";
1 -pub(crate) const MD_EXAMPLE :&str = r"# Ein sehr schöner Titel 1 +CREATE TABLE "markdowns" (
  2 + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
  3 + name VARCHAR(256) NOT NULL,
  4 + content TEXT NOT NULL,
  5 + number_of_versions INTEGER NOT NULL DEFAULT (1),
  6 + date_created TEXT NOT NULL,
  7 + date_updated TEXT NOT NULL
  8 +);
  9 +
  10 +-- This holds compressed reverse patches to markdown.content.
  11 +-- The markdown.content field always holds the latest version.
  12 +-- This way we can always restore every version we had in the past.
  13 +-- The date_created here should be set to the value of
  14 +-- markdown.date_updated when the patch was created. This diff_id
  15 +-- is always current last max diff_id for given markdown_id plus 1.
  16 +CREATE TABLE "markdown_diffs" (
  17 + markdown_id INTEGER NOT NULL,
  18 + diff_id INTEGER NOT NULL,
  19 + diff BLOB NOT NULL,
  20 + date_created TEXT NOT NULL,
  21 + PRIMARY KEY (markdown_id, diff_id)
  22 +);
  23 +
  24 +INSERT INTO
  25 + "markdowns"(name, content, date_created, date_updated)
  26 +VALUES
  27 + ( "md-example"
  28 + , "# Ein sehr schöner Titel
2 29
3 ## Ein sinnloser Text 30 ## Ein sinnloser Text
4 31
@@ -103,30 +130,12 @@ Bananas | 1.89 | 6 @@ -103,30 +130,12 @@ Bananas | 1.89 | 6
103 130
104 und die Spaltenausrichtung kann man auch einstellen: 131 und die Spaltenausrichtung kann man auch einstellen:
105 132
106 -  
107 | Item | Price | # In stock | 133 | Item | Price | # In stock |
108 |--------------|:-----:|-----------:| 134 |--------------|:-----:|-----------:|
109 | Juicy Apples | 1.99 | 739 | 135 | Juicy Apples | 1.99 | 739 |
110 | Bananas | 1.8900 | 6 | 136 | Bananas | 1.8900 | 6 |
111 137
112 -  
113 [lnk1]: https://heise.de/tp/ 'Telepolis' 138 [lnk1]: https://heise.de/tp/ 'Telepolis'
114 -[lnk2]: https://markdown.land/markdown-table 'markdown.land'";  
115 -  
116 -/*  
117 -pub(crate) const PEN_ICON :&str = r#"<?xml version="1.0" encoding="iso-8859-1"?>  
118 -<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"  
119 - "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">  
120 -<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg"  
121 - xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"  
122 - viewBox="0 0 220.001 220.001"  
123 - style="enable-background:new 0 0 220.001 220.001;" xml:space="preserve">  
124 - <g>  
125 - <polygon points="0,220 59.34,213.86 6.143,160.661"/>  
126 - <path d="M132.018,34.787l53.197,53.197L69.568,203.631L16.37,  
127 - 150.434L132.018,34.787z M212.696,60.502c9.738-9.738,9.742-25.527,  
128 - 0-35.268l-17.93-17.93c-9.738-9.74-25.529-9.738-35.268,0l-17.346,  
129 - 17.347l53.199,53.196L212.696,60.502z"/>  
130 - </g>  
131 -</svg>"#;  
132 -*/ 139 +[lnk2]: https://markdown.land/markdown-table 'markdown.land'"
  140 + , "Today"
  141 + , "Today" );
  1 +[package]
  2 +name = "mogwai-list"
  3 +version = "0.0.0"
  4 +authors = ["Georg Hopp <georg@steffers.org>"]
  5 +workspace = ".."
  6 +edition = "2018"
  7 +
  8 +[lib]
  9 +crate-type = ["cdylib", "rlib"]
  10 +
  11 +[features]
  12 +default = ["console_error_panic_hook"]
  13 +
  14 +[dependencies]
  15 +artshop-common = { path = "../common" }
  16 +katex = { version = "0.4", default-features = false, features = ["wasm-js"] }
  17 +pulldown-cmark = "0.9"
  18 +console_log = "^0.1"
  19 +log = "^0.4"
  20 +serde = { version = "^1.0", features = ["derive"] }
  21 +serde_json = "^1.0"
  22 +wasm-bindgen = "^0.2"
  23 +wasm-bindgen-futures = "^0.4"
  24 +
  25 +# The `console_error_panic_hook` crate provides better debugging of panics by
  26 +# logging them with `console.error`. This is great for development, but requires
  27 +# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
  28 +# code size when deploying.
  29 +console_error_panic_hook = { version = "0.1.6", optional = true }
  30 +wee_alloc = { version = "0.4.2", optional = true }
  31 +js-sys = "^0.3"
  32 +
  33 +[dependencies.mogwai]
  34 +version = "^0.5"
  35 +
  36 +[dependencies.web-sys]
  37 +version = "^0.3"
  38 +features = [
  39 + "Document",
  40 + "DomParser",
  41 + "Headers",
  42 + "HtmlElement",
  43 + "HtmlInputElement",
  44 + "MouseEvent",
  45 + "Node",
  46 + "Request",
  47 + "RequestInit",
  48 + "RequestMode",
  49 + "Response",
  50 + "SupportedType",
  51 + "Window",
  52 +]
  53 +
  54 +[dev-dependencies]
  55 +wasm-bindgen-test = "0.2"
  1 +#![allow(unused_braces)]
  2 +use log::Level;
  3 +use mogwai::{futures, prelude::*};
  4 +use std::panic;
  5 +use wasm_bindgen::prelude::*;
  6 +
  7 +/// An item widget.
  8 +/// Keeps track of clicks.
  9 +#[derive(Clone, Debug)]
  10 +struct Item {
  11 + id: usize,
  12 + clicks: Model<u32>,
  13 +}
  14 +
  15 +/// An item's update messages.
  16 +#[derive(Clone)]
  17 +enum ItemMsg {
  18 + /// The user clicked
  19 + Click,
  20 + /// The user requested this item be removed
  21 + Remove,
  22 +}
  23 +
  24 +/// One item's logic loop.
  25 +async fn item_logic(
  26 + id: usize,
  27 + clicks: Model<u32>,
  28 + mut from_view: broadcast::Receiver<ItemMsg>,
  29 + to_list: broadcast::Sender<ListMsg>,
  30 +) {
  31 + loop {
  32 + match from_view.recv().await {
  33 + Ok(ItemMsg::Click) => {
  34 + clicks.visit_mut(|c| *c += 1).await;
  35 + }
  36 + Ok(ItemMsg::Remove) => {
  37 + to_list.broadcast(ListMsg::RemoveItem(id)).await.unwrap();
  38 + break;
  39 + }
  40 + Err(_) => break,
  41 + }
  42 + }
  43 + log::info!("item {} logic loop is done", id);
  44 +}
  45 +
  46 +// ANCHOR: item_view
  47 +fn item_view(
  48 + clicks: impl Stream<Item = u32> + Sendable,
  49 + to_logic: broadcast::Sender<ItemMsg>,
  50 +) -> ViewBuilder<Dom> {
  51 + builder! {
  52 + <li>
  53 + <button
  54 + style:cursor="pointer"
  55 + on:click=to_logic.sink().contra_map(|_| ItemMsg::Click)>
  56 + "Increment"
  57 + </button>
  58 + <button
  59 + style:cursor="pointer"
  60 + on:click=to_logic.sink().contra_map(|_| ItemMsg::Remove)>
  61 + "Remove"
  62 + </button>
  63 + " "
  64 + <span>
  65 + {
  66 + ("", clicks.map(|clicks| match clicks {
  67 + 1 => "1 click".to_string(),
  68 + n => format!("{} clicks", n),
  69 + }))
  70 + }
  71 + </span>
  72 + </li>
  73 + }
  74 +}
  75 +// ANCHOR_END: item_view
  76 +
  77 +/// Create a new item component.
  78 +fn item(id: usize, clicks: Model<u32>, to_list: broadcast::Sender<ListMsg>) -> Component<Dom> {
  79 + let (tx, rx) = broadcast::bounded(1);
  80 + Component::from(item_view(clicks.stream(), tx)).with_logic(item_logic(id, clicks, rx, to_list))
  81 +}
  82 +
  83 +#[derive(Clone)]
  84 +enum ListMsg {
  85 + /// Create a new item
  86 + NewItem,
  87 + /// Remove the item with the given id
  88 + RemoveItem(usize),
  89 +}
  90 +
  91 +// ANCHOR: list_logic_coms
  92 +/// Launch the logic loop of our list of items.
  93 +async fn list_logic(
  94 + input: broadcast::Receiver<ListMsg>,
  95 + tx_patch_children: mpmc::Sender<ListPatch<ViewBuilder<Dom>>>,
  96 +) {
  97 + // Set up our communication from items to this logic loop by
  98 + // * creating a list patch model
  99 + // * creating a channel to go from item to list logic (aka here)
  100 + // * creating a side-effect stream (for_each) that runs for each item patch
  101 + // * map patches of Item to patches of builders and send that to our view
  102 + // through tx_patch_children
  103 + let mut items: ListPatchModel<Item> = ListPatchModel::new();
  104 + let (to_list, from_items) = broadcast::bounded::<ListMsg>(1);
  105 + let to_list = to_list.clone();
  106 + let all_item_patches = items.stream().map(move |patch| {
  107 + log::info!("mapping patch for item: {:?}", patch);
  108 + let to_list = to_list.clone();
  109 + patch.map(move |Item { id, clicks }: Item| {
  110 + let to_list = to_list.clone();
  111 + let component = item(id, clicks, to_list);
  112 + let builder: ViewBuilder<Dom> = component.into();
  113 + builder
  114 + })
  115 + }).for_each(move |patch| {
  116 + let tx_patch_children = tx_patch_children.clone();
  117 + async move {
  118 + tx_patch_children.send(patch).await.unwrap();
  119 + }
  120 + });
  121 + mogwai::spawn(all_item_patches);
  122 + // ANCHOR_END: list_logic_coms
  123 + // ANCHOR: list_logic_loop
  124 + // Combine the input from our view with the input from our items
  125 + let mut input = futures::stream::select_all(vec![input, from_items]);
  126 + let mut next_id = 0;
  127 + loop {
  128 + match input.next().await {
  129 + Some(ListMsg::NewItem) => {
  130 + log::info!("creating a new item");
  131 + let item: Item = Item {
  132 + id: next_id,
  133 + clicks: Model::new(0),
  134 + };
  135 + next_id += 1;
  136 + // patch our items easily and _item_patch_stream's for_each runs automatically,
  137 + // keeping the list of item views in sync
  138 + items.list_patch_push(item);
  139 + }
  140 + Some(ListMsg::RemoveItem(id)) => {
  141 + log::info!("removing item: {}", id);
  142 + let mut may_index = None;
  143 + 'find_item_by_id: for (item, index) in items.read().await.iter().zip(0..) {
  144 + if item.id == id {
  145 + may_index = Some(index);
  146 + break 'find_item_by_id;
  147 + }
  148 + }
  149 +
  150 + if let Some(index) = may_index {
  151 + // patch our items to remove the item at the index
  152 + let _ = items.list_patch_remove(index);
  153 + }
  154 + }
  155 + _ => {
  156 + log::error!("Leaving list logic loop - this shouldn't happen");
  157 + break;
  158 + },
  159 + }
  160 + }
  161 + // ANCHOR_END: list_logic_loop
  162 +}
  163 +
  164 +// ANCHOR: list_view
  165 +fn list_view<T>(to_logic: broadcast::Sender<ListMsg>, children: T) -> ViewBuilder<Dom>
  166 +where
  167 + T: Stream<Item = ListPatch<ViewBuilder<Dom>>> + Sendable,
  168 +{
  169 + builder! {
  170 + <fieldset>
  171 + <legend>"A List of Gizmos"</legend>
  172 + <button style:cursor="pointer" on:click=to_logic.sink().contra_map(|_| ListMsg::NewItem)>
  173 + "Create a new item"
  174 + </button>
  175 + <fieldset>
  176 + <legend>"Items"</legend>
  177 + <ol patch:children=children>
  178 + </ol>
  179 + </fieldset>
  180 + </fieldset>
  181 + }
  182 +}
  183 +// ANCHOR_END: list_view
  184 +
  185 +/// Create our list component.
  186 +fn list() -> Component<Dom> {
  187 + let (logic_tx, logic_rx) = broadcast::bounded(1);
  188 + let (item_patch_tx, item_patch_rx) = mpmc::bounded(1);
  189 + Component::from(list_view(logic_tx, item_patch_rx))
  190 + .with_logic(list_logic(logic_rx, item_patch_tx))
  191 +}
  192 +
  193 +#[wasm_bindgen]
  194 +pub fn main(parent_id: Option<String>) -> Result<(), JsValue> {
  195 + panic::set_hook(Box::new(console_error_panic_hook::hook));
  196 + console_log::init_with_level(Level::Trace).unwrap();
  197 + let component = list();
  198 + let view = component.build().unwrap();
  199 +
  200 + if let Some(id) = parent_id {
  201 + let parent = mogwai::utils::document().get_element_by_id(&id).unwrap();
  202 + view.run_in_container(&parent)
  203 + } else {
  204 + view.run()
  205 + }
  206 +}
@@ -27,6 +27,7 @@ edition = "2018" @@ -27,6 +27,7 @@ edition = "2018"
27 actix-files = "0.2" 27 actix-files = "0.2"
28 actix-web = "2.0" 28 actix-web = "2.0"
29 actix-rt = "1.1.1" 29 actix-rt = "1.1.1"
  30 +artshop-common = { path = "../common" }
30 diesel = { version = "1.4.7", features = ["sqlite", "r2d2"]} 31 diesel = { version = "1.4.7", features = ["sqlite", "r2d2"]}
31 r2d2 = "0.8.9" 32 r2d2 = "0.8.9"
32 dotenv = "0.15.0" 33 dotenv = "0.15.0"
@@ -36,3 +37,5 @@ serde_json = "1.0" @@ -36,3 +37,5 @@ serde_json = "1.0"
36 anyhow = "1.0" 37 anyhow = "1.0"
37 chrono = "0.4.15" 38 chrono = "0.4.15"
38 listenfd = "0.3" 39 listenfd = "0.3"
  40 +diffy = "0.2"
  41 +flate2 = "^1.0"
1 -use std::error::Error as StdError;  
2 -use std::fmt; 1 +use std::{fmt::Display, pin::Pin};
3 2
4 use diesel::result; 3 use diesel::result;
  4 +use diffy::ParsePatchError;
5 use r2d2; 5 use r2d2;
6 6
  7 +type ParentError = Option<Pin<Box<dyn std::error::Error>>>;
  8 +
7 #[derive(Debug)] 9 #[derive(Debug)]
8 -pub(crate) enum Error {  
9 - DieselResult(result::Error),  
10 - DieselR2d2(r2d2::Error), 10 +pub(crate) struct Error {
  11 + source: ParentError,
  12 + message: String,
11 } 13 }
12 14
13 -impl fmt::Display for Error {  
14 - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {  
15 - match self {  
16 - Error::DieselR2d2(r) => write!(f, "{}", r),  
17 - Error::DieselResult(r) => write!(f, "{}", r),  
18 - } 15 +unsafe impl Send for Error {}
  16 +
  17 +pub(crate) type Result<T> = std::result::Result<T, Error>;
  18 +
  19 +impl std::error::Error for Error {
  20 + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
  21 + self.source.as_deref()
19 } 22 }
20 } 23 }
21 24
22 -impl StdError for Error {  
23 - fn source(&self) -> Option<&(dyn StdError + 'static)> { 25 +impl Display for Error {
  26 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24 match self { 27 match self {
25 - Error::DieselR2d2(r) => Some(r),  
26 - Error::DieselResult(r) => Some(r), 28 + Error { source: Some(source), message } =>
  29 + write!(f, "{}: {}", message, source),
  30 + Error { source: None, message } => write!(f, "{}", message),
27 } 31 }
28 } 32 }
29 } 33 }
30 34
  35 +impl From<&str> for Error {
  36 + fn from(message: &str) -> Self {
  37 + Self { source: None, message: String::from(message) }
  38 + }
  39 +}
  40 +
31 impl From<result::Error> for Error { 41 impl From<result::Error> for Error {
32 - fn from(error: result::Error) -> Self {  
33 - Error::DieselResult(error) 42 + fn from(source: result::Error) -> Self {
  43 + Self { source: Some(Box::pin(source))
  44 + , message: String::from("Diesel Result Error")
  45 + }
34 } 46 }
35 } 47 }
36 48
37 impl From<r2d2::Error> for Error { 49 impl From<r2d2::Error> for Error {
38 - fn from(error: r2d2::Error) -> Self {  
39 - Error::DieselR2d2(error) 50 + fn from(source: r2d2::Error) -> Self {
  51 + Self { source: Some(Box::pin(source))
  52 + , message: String::from("Diesel Result Error")
  53 + }
  54 + }
  55 +}
  56 +
  57 +impl From<std::io::Error> for Error {
  58 + fn from(source: std::io::Error) -> Self {
  59 + Self { source: Some(Box::pin(source))
  60 + , message: String::from("IO Error")
  61 + }
  62 + }
  63 +}
  64 +
  65 +impl From<std::str::Utf8Error> for Error {
  66 + fn from(source: std::str::Utf8Error) -> Self {
  67 + Self { source: Some(Box::pin(source))
  68 + , message: String::from("IO Error")
  69 + }
  70 + }
  71 +}
  72 +
  73 +impl From<ParsePatchError> for Error {
  74 + fn from(source: ParsePatchError) -> Self {
  75 + Self { source: Some(Box::pin(source))
  76 + , message: String::from("IO Error")
  77 + }
40 } 78 }
41 } 79 }
@@ -6,10 +6,15 @@ mod models; @@ -6,10 +6,15 @@ mod models;
6 mod routes; 6 mod routes;
7 mod schema; 7 mod schema;
8 8
  9 +use crate::routes::markdown::*;
  10 +use crate::routes::other::*;
  11 +use crate::routes::user::*;
  12 +
9 use actix_web::{guard, web, App, HttpResponse, HttpServer}; 13 use actix_web::{guard, web, App, HttpResponse, HttpServer};
10 use diesel::r2d2::{self, ConnectionManager}; 14 use diesel::r2d2::{self, ConnectionManager};
11 use diesel::SqliteConnection; 15 use diesel::SqliteConnection;
12 use listenfd::ListenFd; 16 use listenfd::ListenFd;
  17 +use routes::markdown::get_markdown;
13 18
14 pub(crate) type Pool = r2d2::Pool<ConnectionManager<SqliteConnection>>; 19 pub(crate) type Pool = r2d2::Pool<ConnectionManager<SqliteConnection>>;
15 20
@@ -28,24 +33,41 @@ async fn main() -> std::io::Result<()> { @@ -28,24 +33,41 @@ async fn main() -> std::io::Result<()> {
28 App::new() . data(database_pool.clone()) 33 App::new() . data(database_pool.clone())
29 . service(actix_files::Files::new("/static", "./static")) 34 . service(actix_files::Files::new("/static", "./static"))
30 . service( web::scope("/api/v0") 35 . service( web::scope("/api/v0")
  36 + . service( web::resource("/markdowns")
  37 + . route(web::get().to(get_markdowns))
  38 + )
  39 + . service( web::resource("/markdowns/{id}")
  40 + . route(web::get().to(get_markdown))
  41 + . route(web::put().to(update_markdown))
  42 + )
  43 + . service( web::resource("/markdowns/{id}/patches")
  44 + . route(web::get().to(get_patches))
  45 + )
31 . service( web::resource("/users") 46 . service( web::resource("/users")
32 - . route(web::get().to(routes::get_users))  
33 - . route(web::put().to(routes::create_user))) 47 + . route(web::get().to(get_users))
  48 + . route(web::put().to(create_user))
  49 + )
34 . service( web::resource("/users/{id}") 50 . service( web::resource("/users/{id}")
35 - . route(web::delete().to(routes::delete_user))  
36 - . route(web::get().to(routes::get_user))  
37 - . route(web::put().to(routes::update_user)))) 51 + . route(web::delete().to(delete_user))
  52 + . route(web::get().to(get_user))
  53 + . route(web::put().to(update_user))
  54 + )
  55 + )
38 . service( web::scope("") 56 . service( web::scope("")
39 - . route("/", web::get().to(routes::root))  
40 - . route("/index", web::get().to(routes::root))  
41 - . route("/index.html", web::get().to(routes::root))  
42 - . route("/favicon", web::get().to(routes::favicon))  
43 - . route("/favicon.ico", web::get().to(routes::favicon)))  
44 - . default_service(web::resource("")  
45 - . route(web::get().to(routes::p404)) 57 + . route("/", web::get().to(root))
  58 + . route("/api.html", web::get().to(apidoc))
  59 + . route("/index", web::get().to(root))
  60 + . route("/index.html", web::get().to(root))
  61 + . route("/favicon", web::get().to(favicon))
  62 + . route("/favicon.ico", web::get().to(favicon))
  63 + )
  64 + . default_service( web::resource("")
  65 + . route( web::get().to(p404) )
46 . route( web::route() 66 . route( web::route()
47 - . guard(guard::Not(guard::Get()))  
48 - . to(HttpResponse::MethodNotAllowed))) 67 + . guard( guard::Not(guard::Get()) )
  68 + . to(HttpResponse::MethodNotAllowed)
  69 + )
  70 + )
49 }); 71 });
50 72
51 let server = match listenfd.take_tcp_listener(0).unwrap() { 73 let server = match listenfd.take_tcp_listener(0).unwrap() {
  1 +use std::sync::Arc;
  2 +use crate::schema::*;
  3 +use crate::error::*;
  4 +use crate::Pool;
  5 +use artshop_common::types::MarkdownDiffJson;
  6 +use artshop_common::types::MarkdownJson;
  7 +use diesel::prelude::*;
  8 +use diesel::{
  9 + dsl::{delete, insert_into, update},
  10 + RunQueryDsl
  11 +};
  12 +use serde::{Deserialize, Serialize};
  13 +use std::io::Write;
  14 +use diffy::{apply, create_patch, Patch};
  15 +use flate2::Compression;
  16 +use flate2::write::{DeflateEncoder, DeflateDecoder};
  17 +
  18 +
  19 +#[derive(Debug, Clone, Serialize, Deserialize, Queryable, Identifiable)]
  20 +pub struct Markdown {
  21 + pub id: i32,
  22 + pub name: String,
  23 + pub content: String,
  24 + pub number_of_versions: i32,
  25 + pub date_created: String,
  26 + pub date_updated: String,
  27 +}
  28 +
  29 +#[derive(Debug, Serialize, Deserialize, Queryable, Identifiable)]
  30 +#[table_name = "markdown_diffs"]
  31 +#[primary_key(markdown_id, diff_id)]
  32 +pub struct MarkdownDiff {
  33 + pub markdown_id: i32,
  34 + pub diff_id: i32,
  35 + pub diff: Vec<u8>,
  36 + pub date_created: String,
  37 +}
  38 +
  39 +#[derive(Debug, Insertable)]
  40 +#[table_name = "markdowns"]
  41 +pub struct MarkdownNew<'a> {
  42 + pub name: &'a str,
  43 + pub content: &'a str,
  44 + pub number_of_versions: i32,
  45 + pub date_created: &'a str,
  46 + pub date_updated: &'a str,
  47 +}
  48 +
  49 +#[derive(Debug, Insertable)]
  50 +#[table_name = "markdown_diffs"]
  51 +pub struct MarkdownDiffNew<'a> {
  52 + pub markdown_id: i32,
  53 + pub diff_id: i32,
  54 + pub diff: &'a [u8],
  55 + pub date_created: &'a str,
  56 +}
  57 +
  58 +#[derive(Debug, Serialize, Deserialize, AsChangeset)]
  59 +#[table_name="markdowns"]
  60 +struct MarkdownChange<'a> {
  61 + pub name: &'a str,
  62 + pub content: &'a str,
  63 + pub number_of_versions: i32,
  64 + pub date_created: &'a str,
  65 + pub date_updated: &'a str,
  66 +}
  67 +
  68 +impl<'a> From<&'a MarkdownJson> for MarkdownChange<'a> {
  69 + fn from(md: &'a MarkdownJson) -> Self {
  70 + Self { name: md.name.as_str()
  71 + , content: md.content.as_str()
  72 + , number_of_versions: md.number_of_versions
  73 + , date_created: md.date_created.as_str()
  74 + , date_updated: md.date_updated.as_str()
  75 + }
  76 + }
  77 +}
  78 +
  79 +impl From<&MarkdownDiff> for MarkdownDiffJson {
  80 + fn from(md_diff: &MarkdownDiff) -> Self {
  81 + Self { id: md_diff.diff_id
  82 + , date_created: md_diff.date_created.to_owned()
  83 + }
  84 + }
  85 +}
  86 +
  87 +impl MarkdownDiff {
  88 + pub(crate) fn get_diff_as_string(&self) -> Result<String> {
  89 + let mut decoder = DeflateDecoder::new(Vec::new());
  90 +
  91 + decoder.write_all(self.diff.as_ref())?;
  92 + let decomp = decoder.finish()?;
  93 +
  94 + Ok(String::from(std::str::from_utf8(decomp.as_ref())?))
  95 + }
  96 +}
  97 +
  98 +pub(crate) enum _Action {
  99 + Created(Markdown),
  100 + Found(Markdown),
  101 +}
  102 +
  103 +
  104 +pub(crate) fn _create_markdown( pool: Arc<Pool>
  105 + , item: MarkdownJson ) -> Result<_Action> {
  106 + use crate::schema::markdowns::dsl::*;
  107 + let db_connection = pool.get()?;
  108 +
  109 + match markdowns . filter(name.eq(&item.name))
  110 + . first::<Markdown>(&db_connection)
  111 + {
  112 + Ok(result) => Ok(_Action::Found(result)),
  113 + Err(_) => {
  114 + let now = chrono::Local::now().naive_local();
  115 + let new_markdown = MarkdownNew {
  116 + name: &item.name,
  117 + content: &item.content,
  118 + number_of_versions: item.number_of_versions,
  119 + date_created: &format!("{}", now),
  120 + date_updated: &format!("{}", now),
  121 + };
  122 +
  123 + Ok(_Action::Created(db_connection.transaction(|| {
  124 + insert_into(markdowns) . values(&new_markdown)
  125 + . execute(&db_connection)?;
  126 +
  127 + markdowns . order(id.desc())
  128 + . first::<Markdown>(&db_connection)
  129 + })?))
  130 + }
  131 + }
  132 +}
  133 +
  134 +pub(crate) fn get_markdowns(pool: Arc<Pool>) -> Result<Vec<Markdown>>
  135 +{
  136 + use crate::schema::markdowns::dsl::*;
  137 + let db_connection = pool.get()?;
  138 + Ok(markdowns.load::<Markdown>(&db_connection)?)
  139 +}
  140 +
  141 +pub(crate) fn get_markdown( pool: Arc<Pool>
  142 + , ident: &str
  143 + , patch: Option<i32> ) -> Result<Markdown>
  144 +{
  145 + use crate::schema::markdowns::dsl::*;
  146 + use crate::schema::markdown_diffs::dsl::*;
  147 +
  148 + let db_connection = pool.get()?;
  149 +
  150 + let mut markdown = markdowns
  151 + . filter(name.eq(ident))
  152 + . first::<Markdown>(&db_connection)?;
  153 +
  154 + if let Some(patch) = patch {
  155 + let result = markdown_diffs
  156 + . filter(markdown_id.eq(markdown.id))
  157 + . filter(diff_id.ge(patch))
  158 + . order(diff_id.desc())
  159 + . load::<MarkdownDiff>(&db_connection)?;
  160 +
  161 + for patch in result {
  162 + let patch_data = patch.get_diff_as_string()?;
  163 + let decomp = Patch::from_str(&patch_data)?;
  164 +
  165 + markdown.content = apply(&markdown.content, &decomp).unwrap();
  166 + markdown.date_updated = patch.date_created;
  167 + }
  168 + };
  169 +
  170 + Ok(markdown)
  171 +}
  172 +
  173 +pub(crate) fn get_patches( pool: Arc<Pool>
  174 + , ident: &str ) -> Result<Vec<MarkdownDiffJson>> {
  175 + use crate::schema::markdowns::dsl::*;
  176 + use crate::schema::markdown_diffs::dsl::*;
  177 +
  178 + let db_connection = pool.get()?;
  179 +
  180 + let markdown = markdowns
  181 + . filter(name.eq(ident))
  182 + . first::<Markdown>(&db_connection)?;
  183 +
  184 + Ok ( markdown_diffs . filter(markdown_id.eq(markdown.id))
  185 + . order(diff_id.desc())
  186 + . load::<MarkdownDiff>(&db_connection)?
  187 + . iter()
  188 + . map(|d| MarkdownDiffJson::from(d))
  189 + . collect() )
  190 +}
  191 +
  192 +pub(crate) fn _delete_markdown( pool: Arc<Pool>
  193 + , ident: i32 ) -> Result<usize>
  194 +{
  195 + use crate::schema::markdowns::dsl::*;
  196 + let db_connection = pool.get()?;
  197 + Ok(delete(markdowns.find(ident)).execute(&db_connection)?)
  198 +}
  199 +
  200 +pub(crate) fn update_markdown( pool: Arc<Pool>
  201 + , ident: String
  202 + , mut item: MarkdownJson ) -> Result<Markdown>
  203 +{
  204 + use crate::schema::markdowns::dsl::*;
  205 + use crate::schema::markdown_diffs::dsl::*;
  206 + let db_connection = pool.get()?;
  207 + let mut markdown = markdowns
  208 + . filter(name.eq(ident))
  209 + . first::<Markdown>(&db_connection)?;
  210 +
  211 + let patch = format!( "{}", create_patch( item.content.as_str()
  212 + , markdown.content.as_str() ));
  213 + let mut encoder = DeflateEncoder::new(Vec::new(), Compression::best());
  214 + encoder.write_all(patch.as_bytes()).unwrap();
  215 + let compressed = encoder.finish().unwrap();
  216 +
  217 + let last_diff = match markdown_diffs . filter(markdown_id.eq(markdown.id))
  218 + . order(diff_id.desc())
  219 + . first::<MarkdownDiff>(&db_connection)
  220 + {
  221 + Ok(result) => (result.diff_id, Some(result)),
  222 + Err(_) => (0, None),
  223 + };
  224 +
  225 + let now = chrono::Local::now().naive_local();
  226 + let new_markdown_diff = MarkdownDiffNew {
  227 + markdown_id: markdown.id,
  228 + diff_id: last_diff.0 + 1,
  229 + diff: compressed.as_ref(),
  230 + date_created: markdown.date_updated.as_str(),
  231 + };
  232 +
  233 + item.date_updated = format!("{}", now);
  234 + item.number_of_versions = item.number_of_versions + 1;
  235 +
  236 + db_connection.transaction::<_, Error, _>(|| {
  237 + insert_into(markdown_diffs) . values(&new_markdown_diff)
  238 + . execute(&db_connection)?;
  239 +
  240 + update(&markdown).set(MarkdownChange::from(&item)).execute(&db_connection)?;
  241 +
  242 + Ok(())
  243 + }).unwrap();
  244 +
  245 + markdown.name = item.name;
  246 + markdown.content = item.content;
  247 + markdown.number_of_versions = item.number_of_versions;
  248 + markdown.date_updated = item.date_updated;
  249 +
  250 + Ok(markdown)
  251 +}
  1 +pub(crate) mod user;
  2 +pub(crate) mod markdown;
1 use std::sync::Arc; 1 use std::sync::Arc;
2 use crate::schema::*; 2 use crate::schema::*;
3 -use crate::error::Error; 3 +use crate::error::*;
4 use crate::Pool; 4 use crate::Pool;
5 use diesel::prelude::*; 5 use diesel::prelude::*;
6 use diesel::{ 6 use diesel::{
@@ -40,7 +40,7 @@ pub(crate) enum Action { @@ -40,7 +40,7 @@ pub(crate) enum Action {
40 40
41 41
42 pub(crate) fn create_user( pool: Arc<Pool> 42 pub(crate) fn create_user( pool: Arc<Pool>
43 - , item: UserJson ) -> Result<Action, Error> { 43 + , item: UserJson ) -> Result<Action> {
44 use crate::schema::users::dsl::*; 44 use crate::schema::users::dsl::*;
45 let db_connection = pool.get()?; 45 let db_connection = pool.get()?;
46 46
@@ -67,7 +67,7 @@ pub(crate) fn create_user( pool: Arc<Pool> @@ -67,7 +67,7 @@ pub(crate) fn create_user( pool: Arc<Pool>
67 } 67 }
68 } 68 }
69 69
70 -pub(crate) fn get_users(pool: Arc<Pool>) -> Result<Vec<User>, Error> 70 +pub(crate) fn get_users(pool: Arc<Pool>) -> Result<Vec<User>>
71 { 71 {
72 use crate::schema::users::dsl::*; 72 use crate::schema::users::dsl::*;
73 let db_connection = pool.get()?; 73 let db_connection = pool.get()?;
@@ -75,7 +75,7 @@ pub(crate) fn get_users(pool: Arc<Pool>) -> Result<Vec<User>, Error> @@ -75,7 +75,7 @@ pub(crate) fn get_users(pool: Arc<Pool>) -> Result<Vec<User>, Error>
75 } 75 }
76 76
77 pub(crate) fn get_user( pool: Arc<Pool> 77 pub(crate) fn get_user( pool: Arc<Pool>
78 - , ident: i32 ) -> Result<User, Error> 78 + , ident: i32 ) -> Result<User>
79 { 79 {
80 use crate::schema::users::dsl::*; 80 use crate::schema::users::dsl::*;
81 let db_connection = pool.get()?; 81 let db_connection = pool.get()?;
@@ -83,7 +83,7 @@ pub(crate) fn get_user( pool: Arc<Pool> @@ -83,7 +83,7 @@ pub(crate) fn get_user( pool: Arc<Pool>
83 } 83 }
84 84
85 pub(crate) fn delete_user( pool: Arc<Pool> 85 pub(crate) fn delete_user( pool: Arc<Pool>
86 - , ident: i32 ) -> Result<usize, Error> 86 + , ident: i32 ) -> Result<usize>
87 { 87 {
88 use crate::schema::users::dsl::*; 88 use crate::schema::users::dsl::*;
89 let db_connection = pool.get()?; 89 let db_connection = pool.get()?;
@@ -92,7 +92,7 @@ pub(crate) fn delete_user( pool: Arc<Pool> @@ -92,7 +92,7 @@ pub(crate) fn delete_user( pool: Arc<Pool>
92 92
93 pub(crate) fn update_user( pool: Arc<Pool> 93 pub(crate) fn update_user( pool: Arc<Pool>
94 , ident: i32 94 , ident: i32
95 - , item: UserJson ) -> Result<User, Error> 95 + , item: UserJson ) -> Result<User>
96 { 96 {
97 use crate::schema::users::dsl::*; 97 use crate::schema::users::dsl::*;
98 let db_connection = pool.get()?; 98 let db_connection = pool.get()?;
  1 +use crate::models::markdown;
  2 +use crate::Pool;
  3 +
  4 +use actix_web::{Error, HttpResponse, web};
  5 +use anyhow::Result;
  6 +use artshop_common::types::MarkdownJson;
  7 +use serde::Deserialize;
  8 +
  9 +#[derive(Debug, Deserialize)]
  10 +pub struct Patchset {
  11 + patch: Option<i32>,
  12 +}
  13 +
  14 +pub async fn get_markdowns(pool: web::Data<Pool>)
  15 + -> Result<HttpResponse, Error>
  16 +{
  17 + Ok( web::block(move || markdown::get_markdowns(pool.into_inner()))
  18 + . await
  19 + . map(|markdowns| HttpResponse::Ok().json(markdowns))
  20 + . map_err(|_| HttpResponse::InternalServerError())?
  21 + )
  22 +}
  23 +
  24 +pub async fn get_markdown( pool: web::Data<Pool>
  25 + , name: web::Path<String>
  26 + , patch: web::Query<Patchset>
  27 + ) -> Result<HttpResponse, Error>
  28 +{
  29 + let pool = pool.into_inner();
  30 + let name = name.into_inner();
  31 + let patch = patch.into_inner();
  32 +
  33 + Ok( web::block(move || markdown::get_markdown(pool, name.as_str(), patch.patch))
  34 + . await
  35 + . map(|markdowns| HttpResponse::Ok().json(markdowns))
  36 + . map_err(|_| HttpResponse::InternalServerError())?
  37 + )
  38 +}
  39 +
  40 +pub async fn get_patches( pool: web::Data<Pool>
  41 + , name: web::Path<String>
  42 + ) -> Result<HttpResponse, Error> {
  43 + let pool = pool.into_inner();
  44 + let name = name.into_inner();
  45 +
  46 + Ok( web::block(move || markdown::get_patches(pool, name.as_str()))
  47 + . await
  48 + . map(|patches| HttpResponse::Ok().json(patches))
  49 + . map_err(|_| HttpResponse::InternalServerError())?
  50 + )
  51 +}
  52 +
  53 +pub async fn update_markdown( pool: web::Data<Pool>
  54 + , name: web::Path<String>
  55 + , item: web::Json<MarkdownJson> )
  56 + -> Result<HttpResponse, Error>
  57 +{
  58 + let pool = pool.into_inner();
  59 + let name = name.into_inner();
  60 + let item = item.into_inner();
  61 +
  62 + Ok(web::block(move || markdown::update_markdown(pool, name, item))
  63 + . await
  64 + . map(|markdown| HttpResponse::Ok().json(markdown))
  65 + . map_err(|_| HttpResponse::InternalServerError())?)
  66 +}
  1 +pub(crate) mod markdown;
  2 +pub(crate) mod other;
  3 +pub(crate) mod user;
  1 +use actix_web::Error;
  2 +use anyhow::Result;
  3 +
  4 +pub async fn root() -> Result<actix_files::NamedFile, Error> {
  5 + Ok(actix_files::NamedFile::open("static/index.html")?)
  6 +}
  7 +
  8 +pub async fn apidoc() -> Result<actix_files::NamedFile, Error> {
  9 + Ok(actix_files::NamedFile::open("static/api.html")?)
  10 +}
  11 +
  12 +pub async fn p404() -> Result<actix_files::NamedFile, Error> {
  13 + Ok(actix_files::NamedFile::open("static/404.html")?)
  14 +}
  15 +
  16 +pub async fn favicon() -> Result<actix_files::NamedFile, Error> {
  17 + Ok(actix_files::NamedFile::open("static/favicon.ico")?)
  18 +}
1 -use crate::models::{self, Action}; 1 +use crate::models::user::{self, Action};
2 use crate::Pool; 2 use crate::Pool;
3 3
4 use actix_web::{Error, HttpResponse, web}; 4 use actix_web::{Error, HttpResponse, web};
5 use anyhow::Result; 5 use anyhow::Result;
6 6
7 -pub async fn root() -> Result<actix_files::NamedFile, Error> {  
8 - Ok(actix_files::NamedFile::open("static/index.html")?)  
9 -}  
10 -  
11 -pub async fn p404() -> Result<actix_files::NamedFile, Error> {  
12 - Ok(actix_files::NamedFile::open("static/404.html")?)  
13 -}  
14 -  
15 -pub async fn favicon() -> Result<actix_files::NamedFile, Error> {  
16 - Ok(actix_files::NamedFile::open("static/favicon.ico")?)  
17 -}  
18 -  
19 pub async fn create_user( pool: web::Data<Pool> 7 pub async fn create_user( pool: web::Data<Pool>
20 - , item: web::Json<models::UserJson> ) 8 + , item: web::Json<user::UserJson> )
21 -> Result<HttpResponse, Error> 9 -> Result<HttpResponse, Error>
22 { 10 {
23 let pool = pool.into_inner(); 11 let pool = pool.into_inner();
24 let item = item.into_inner(); 12 let item = item.into_inner();
25 13
26 - Ok(web::block(move || models::create_user(pool, item)) 14 + Ok(web::block(move || user::create_user(pool, item))
27 . await 15 . await
28 . map(|action| { 16 . map(|action| {
29 match action { 17 match action {
@@ -36,7 +24,7 @@ pub async fn create_user( pool: web::Data<Pool> @@ -36,7 +24,7 @@ pub async fn create_user( pool: web::Data<Pool>
36 pub async fn get_users(pool: web::Data<Pool>) 24 pub async fn get_users(pool: web::Data<Pool>)
37 -> Result<HttpResponse, Error> 25 -> Result<HttpResponse, Error>
38 { 26 {
39 - Ok(web::block(move || models::get_users(pool.into_inner())) 27 + Ok(web::block(move || user::get_users(pool.into_inner()))
40 . await 28 . await
41 . map(|users| HttpResponse::Ok().json(users)) 29 . map(|users| HttpResponse::Ok().json(users))
42 . map_err(|_| HttpResponse::InternalServerError())?) 30 . map_err(|_| HttpResponse::InternalServerError())?)
@@ -48,7 +36,7 @@ pub async fn get_user(pool: web::Data<Pool>, id: web::Path<i32>) @@ -48,7 +36,7 @@ pub async fn get_user(pool: web::Data<Pool>, id: web::Path<i32>)
48 let pool = pool.into_inner(); 36 let pool = pool.into_inner();
49 let id = id.into_inner(); 37 let id = id.into_inner();
50 38
51 - Ok(web::block(move || models::get_user(pool, id)) 39 + Ok(web::block(move || user::get_user(pool, id))
52 . await 40 . await
53 . map(|user| HttpResponse::Ok().json(user)) 41 . map(|user| HttpResponse::Ok().json(user))
54 . map_err(|_| HttpResponse::InternalServerError())?) 42 . map_err(|_| HttpResponse::InternalServerError())?)
@@ -60,7 +48,7 @@ pub async fn delete_user(pool: web::Data<Pool>, id: web::Path<i32>) @@ -60,7 +48,7 @@ pub async fn delete_user(pool: web::Data<Pool>, id: web::Path<i32>)
60 let pool = pool.into_inner(); 48 let pool = pool.into_inner();
61 let id = id.into_inner(); 49 let id = id.into_inner();
62 50
63 - Ok(web::block(move || models::delete_user(pool, id)) 51 + Ok(web::block(move || user::delete_user(pool, id))
64 . await 52 . await
65 . map(|_| HttpResponse::NoContent().finish()) 53 . map(|_| HttpResponse::NoContent().finish())
66 . map_err(|_| HttpResponse::InternalServerError())?) 54 . map_err(|_| HttpResponse::InternalServerError())?)
@@ -68,14 +56,14 @@ pub async fn delete_user(pool: web::Data<Pool>, id: web::Path<i32>) @@ -68,14 +56,14 @@ pub async fn delete_user(pool: web::Data<Pool>, id: web::Path<i32>)
68 56
69 pub async fn update_user( pool: web::Data<Pool> 57 pub async fn update_user( pool: web::Data<Pool>
70 , id: web::Path<i32> 58 , id: web::Path<i32>
71 - , item: web::Json<models::UserJson> ) 59 + , item: web::Json<user::UserJson> )
72 -> Result<HttpResponse, Error> 60 -> Result<HttpResponse, Error>
73 { 61 {
74 let pool = pool.into_inner(); 62 let pool = pool.into_inner();
75 let id = id.into_inner(); 63 let id = id.into_inner();
76 let item = item.into_inner(); 64 let item = item.into_inner();
77 65
78 - Ok(web::block(move || models::update_user(pool, id, item)) 66 + Ok(web::block(move || user::update_user(pool, id, item))
79 . await 67 . await
80 . map(|user| HttpResponse::Ok().json(user)) 68 . map(|user| HttpResponse::Ok().json(user))
81 . map_err(|_| HttpResponse::InternalServerError())?) 69 . map_err(|_| HttpResponse::InternalServerError())?)
1 table! { 1 table! {
  2 + markdown_diffs (markdown_id, diff_id) {
  3 + markdown_id -> Integer,
  4 + diff_id -> Integer,
  5 + diff -> Binary,
  6 + date_created -> Text,
  7 + }
  8 +}
  9 +
  10 +table! {
  11 + markdowns (id) {
  12 + id -> Integer,
  13 + name -> Text,
  14 + content -> Text,
  15 + number_of_versions -> Integer,
  16 + date_created -> Text,
  17 + date_updated -> Text,
  18 + }
  19 +}
  20 +
  21 +table! {
2 users (id) { 22 users (id) {
3 id -> Integer, 23 id -> Integer,
4 name -> Text, 24 name -> Text,
@@ -6,3 +26,9 @@ table! { @@ -6,3 +26,9 @@ table! {
6 date_created -> Text, 26 date_created -> Text,
7 } 27 }
8 } 28 }
  29 +
  30 +allow_tables_to_appear_in_same_query!(
  31 + markdown_diffs,
  32 + markdowns,
  33 + users,
  34 +);
  1 +<!doctype html>
  2 +<html lang="en">
  3 + <head>
  4 + <meta charset="utf-8">
  5 + <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  6 + <title>Elements in HTML</title>
  7 + <!-- Embed elements Elements via Web Component -->
  8 + <script src="https://unpkg.com/@stoplight/elements/web-components.min.js"></script>
  9 + <link rel="stylesheet" href="https://unpkg.com/@stoplight/elements/styles.min.css">
  10 + </head>
  11 + <body>
  12 +
  13 + <elements-api
  14 + apiDescriptionUrl="/static/openapi.yaml"
  15 + router="hash"
  16 + layout="sidebar"
  17 + />
  18 +
  19 + </body>
  20 +</html>
  1 +openapi: 3.0.0
  2 +info:
  3 + version: 0.0.1
  4 + title: Artshop API
  5 + description: A first simple try do document out api
  6 +paths:
  7 + /api/v0/markdowns:
  8 + get:
  9 + description: Returns a list of all available markdown documents.
  10 + responses:
  11 + '200':
  12 + description: Successful response
@@ -22,6 +22,10 @@ @@ -22,6 +22,10 @@
22 width: 100%; 22 width: 100%;
23 overflow-y: scroll; 23 overflow-y: scroll;
24 overflow-x: hidden; 24 overflow-x: hidden;
  25 + background: #ffffff;
  26 + border-radius: .35em;
  27 + border: 2px solid #bbb;
  28 + padding: 4px;
25 } 29 }
26 30
27 .input > div:last-child { 31 .input > div:last-child {
@@ -12,13 +12,15 @@ crate-type = ["cdylib", "rlib"] @@ -12,13 +12,15 @@ crate-type = ["cdylib", "rlib"]
12 default = ["console_error_panic_hook"] 12 default = ["console_error_panic_hook"]
13 13
14 [dependencies] 14 [dependencies]
  15 +artshop-common = { path = "../common" }
  16 +katex = { version = "0.4", default-features = false, features = ["wasm-js"] }
15 pulldown-cmark = "0.9" 17 pulldown-cmark = "0.9"
16 console_log = "^0.1" 18 console_log = "^0.1"
17 log = "^0.4" 19 log = "^0.4"
18 serde = { version = "^1.0", features = ["derive"] } 20 serde = { version = "^1.0", features = ["derive"] }
19 serde_json = "^1.0" 21 serde_json = "^1.0"
20 wasm-bindgen = "^0.2" 22 wasm-bindgen = "^0.2"
21 -wasm-bindgen-futures = "0.4" 23 +wasm-bindgen-futures = "^0.4"
22 24
23 # The `console_error_panic_hook` crate provides better debugging of panics by 25 # The `console_error_panic_hook` crate provides better debugging of panics by
24 # logging them with `console.error`. This is great for development, but requires 26 # logging them with `console.error`. This is great for development, but requires
@@ -26,6 +28,7 @@ wasm-bindgen-futures = "0.4" @@ -26,6 +28,7 @@ wasm-bindgen-futures = "0.4"
26 # code size when deploying. 28 # code size when deploying.
27 console_error_panic_hook = { version = "0.1.6", optional = true } 29 console_error_panic_hook = { version = "0.1.6", optional = true }
28 wee_alloc = { version = "0.4.2", optional = true } 30 wee_alloc = { version = "0.4.2", optional = true }
  31 +js-sys = "^0.3"
29 32
30 [dependencies.mogwai] 33 [dependencies.mogwai]
31 version = "^0.5" 34 version = "^0.5"
@@ -38,6 +41,7 @@ features = [ @@ -38,6 +41,7 @@ features = [
38 "Headers", 41 "Headers",
39 "HtmlElement", 42 "HtmlElement",
40 "HtmlInputElement", 43 "HtmlInputElement",
  44 + "MouseEvent",
41 "Node", 45 "Node",
42 "Request", 46 "Request",
43 "RequestInit", 47 "RequestInit",
  1 +use std::fmt::Display;
  2 +
  3 +use artshop_common::types::{MarkdownJson, MarkdownDiffJson};
  4 +
  5 +use super::super::error::*;
  6 +use super::super::client::Client;
  7 +
  8 +#[derive(Debug, Clone)]
  9 +pub struct Markdown {
  10 + client: Client,
  11 + pub json: MarkdownJson,
  12 +}
  13 +
  14 +impl Markdown {
  15 + pub(crate) async fn new(name :&str) -> Result<Markdown> {
  16 + let client = Client::new()?;
  17 + let api_uri = format!("/api/v0/markdowns/{}", name);
  18 + let (response, data) = client.get(&api_uri).await?;
  19 +
  20 + match response.status() {
  21 + 200 => Ok(Self { client
  22 + , json: serde_json::from_str(data.as_str())? }),
  23 + status => Err(Self::status_error(status)),
  24 + }
  25 + }
  26 +
  27 + pub(crate) async fn read( &mut self
  28 + , patch :Option<i32>
  29 + ) -> Result<&Markdown>
  30 + {
  31 + let api_uri = match patch {
  32 + Some(i) => format!( "/api/v0/markdowns/{}?patch={}"
  33 + , self.json.name, i ),
  34 + None => format!("/api/v0/markdowns/{}", self.json.name),
  35 + };
  36 + let (response, data) = self.client.get(&api_uri).await?;
  37 +
  38 + match response.status() {
  39 + 200 => {
  40 + self.json = serde_json::from_str(data.as_str())?;
  41 + Ok(self)
  42 + },
  43 + status => Err(Self::status_error(status)),
  44 + }
  45 + }
  46 +
  47 + pub(crate) async fn save(&self) -> Result<&Markdown> {
  48 + let url = format!("/api/v0/markdowns/{}", self.json.name);
  49 + let data = serde_json::to_string(&self.json)?;
  50 +
  51 + let response = self.client.put(url.as_str(), data.as_str()).await?;
  52 +
  53 + match response.status() {
  54 + 200 => Ok(self),
  55 + status => Err(Self::status_error(status)),
  56 + }
  57 + }
  58 +
  59 + pub(crate) async fn patches(&self) -> Result<Vec<MarkdownDiffJson>> {
  60 + let url = format!("/api/v0/markdowns/{}/patches", self.json.name);
  61 + let (response, data) = self.client.get(url.as_str()).await?;
  62 +
  63 + match response.status() {
  64 + 200 => Ok(serde_json::from_str(&data)?),
  65 + status => Err(Self::status_error(status)),
  66 + }
  67 + }
  68 +
  69 + pub(crate) fn _to_html_string(&self) -> String {
  70 + use pulldown_cmark::{Parser, Options, html};
  71 +
  72 + let mut html_out = String::new();
  73 + let parser = Parser::new_ext(&self.json.content, Options::all());
  74 +
  75 + html::push_html(&mut html_out, parser);
  76 + html_out
  77 + }
  78 +
  79 +
  80 + fn status_error<I: Display>(status :I) -> Error {
  81 + let err_str = format!("Invalid response status: {}", status);
  82 + Error::from(err_str.as_str())
  83 + }
  84 +}
  1 +pub(crate) mod markdown;
  1 +use js_sys::JsString;
  2 +use mogwai::prelude::*;
  3 +use wasm_bindgen::prelude::*;
  4 +use web_sys::{Window, window, Response, Request, RequestInit, RequestMode};
  5 +use super::error::*;
  6 +
  7 +use std::result::Result as StdResult;
  8 +
  9 +#[derive(Debug, Clone)]
  10 +pub(crate) struct Client {
  11 + window :Window,
  12 +}
  13 +
  14 +type ReqGetter = fn(&str, &RequestInit) -> StdResult<Request, JsValue>;
  15 +
  16 +const REQUEST :ReqGetter = Request::new_with_str_and_init;
  17 +
  18 +impl Client {
  19 + pub fn new() -> Result<Self> {
  20 + const WINDOW_ERROR :&str = "Unable to get window instance";
  21 +
  22 + Ok(Self { window: window()
  23 + . ok_or(Error::from(WINDOW_ERROR))? })
  24 + }
  25 +
  26 + pub async fn get(&self, url :&str) -> Result<(Response, String)> {
  27 + let mut init = RequestInit::new();
  28 + let request = REQUEST( &url
  29 + , init . method("GET")
  30 + . mode(RequestMode::Cors) )?;
  31 +
  32 + request . headers()
  33 + . set("Accept", "application/json")?;
  34 +
  35 + let response = JsFuture::from( self.window
  36 + . fetch_with_request(&request) )
  37 + . await?
  38 + . dyn_into::<Response>()?;
  39 +
  40 + let data = JsFuture::from(response.text()?)
  41 + . await?
  42 + . dyn_into::<JsString>()?;
  43 +
  44 + Ok((response, String::from(data)))
  45 + }
  46 +
  47 + pub async fn put(&self, url :&str, data :&str) -> Result<Response> {
  48 + let mut init = RequestInit::new();
  49 + let request = REQUEST( &url
  50 + , init . method("PUT")
  51 + . mode(RequestMode::Cors)
  52 + . body(Some(&data.into())) )?;
  53 +
  54 + request . headers()
  55 + . set("Content-Type", "application/json")?;
  56 +
  57 + let response = JsFuture::from( self.window
  58 + . fetch_with_request(&request))
  59 + . await?
  60 + . dyn_into::<Response>()?;
  61 +
  62 + Ok(response)
  63 + }
  64 +}
  1 +use mogwai::prelude::*;
  2 +use super::NS;
  3 +
  4 +pub(crate) fn discard_icon() -> ViewBuilder<Dom> {
  5 + builder! {
  6 + <svg xmlns=NS viewBox="10.3 3.8 76.2 88.7"
  7 + style:width="1.5em" style:height="1.5em">
  8 + <g xmlns=NS id="_x37_0">
  9 + <polygon xmlns=NS
  10 + points="35.4,18.1 31.1,4.3 31.1,16.3
  11 + 24.5,16.3 24.5,10.9 18.3,18.9
  12 + 13.8,23.7 17,23.7 19.8,31.8
  13 + 27.9,34.1 27.9,37.8 38.7,41.8
  14 + 38.7,34.1 38.7,30.4 38.7,25.2
  15 + 46.8,25.2 "/>
  16 + <path xmlns=NS
  17 + d="M75.7,39.9 h-8.3 h-40.5 h-8.3 h-3 v9.5
  18 + v2 h4.7 c2.2,13.3,6.8,42.2,6.8,42.2 h40
  19 + c0,0,4.6-27.6,7-42.2 h4.7 v-2 v-9.5
  20 + H75.7z
  21 + M67.7,51.4 l-2.6,15.6 l-6.1-6.9 l8-8.7
  22 + H67.7z
  23 + M63.7,75.6 l-1.3,7.9 l-2.9-3.2
  24 + L63.7,75.6z
  25 + M56.6,77.2
  26 + L50,70 l6.2-6.7 l6.4,7.3
  27 + L56.6,77.2z
  28 + M52.4,51.4 h8.9 l-5.1,5.5 l-4.8-5.4
  29 + C51.8,51.4,52.1,51.4,52.4,51.4z
  30 + M53.3,60 l-6.2,6.8 l-6.6-7.2 l6.4-6.9
  31 + L53.3,60z
  32 + M30.5,74.9 l4.6,5 l-3.2,3.5
  33 + L30.5,74.9z
  34 + M31.4,69.6 l6.2-6.8 l6.6,7.2 l-6.2,6.8
  35 + L31.4,69.6z
  36 + M41.9,51.4 c0.1,0,0.2,0,0.3,0 l-4.7,5.1
  37 + l-4.7-5.1
  38 + H41.9z
  39 + M27.1,51.4 l7.6,8.3 l-5.7,6.2l-2.3-14.5
  40 + H27.1z
  41 + M34.6,86.7 l3.3-3.6 l3.3,3.6
  42 + H34.6z
  43 + M47.1,86.7 l-6.2-6.8 l6.2-6.8 l6.6,7.2
  44 + l-5.8,6.4
  45 + H47.1z
  46 + M53.7,86.7 l2.9-3.2 l3,3.2
  47 + H53.7z"/>
  48 + </g>
  49 + </svg>
  50 + }
  51 +}
  1 +use mogwai::prelude::*;
  2 +use super::NS;
  3 +
  4 +pub(crate) fn edit_icon() -> ViewBuilder<Dom> {
  5 + builder! {
  6 + <svg xmlns=NS viewBox="0 0 220.001 220.001"
  7 + style:width="1.5em" style:height="1.5em">
  8 + <g xmlns=NS>
  9 + <polygon xmlns=NS
  10 + points="0,220 59.34,213.86 6.143,160.661"/>
  11 + <path xmlns=NS
  12 + d="M132.018,34.787 l53.197,53.197
  13 + L69.568,203.631
  14 + L16.37,150.434
  15 + L132.018,34.787z
  16 + M212.696,60.502
  17 + c9.738,-9.738,9.742,-25.527,0,-35.268
  18 + l-17.93,-17.93
  19 + c-9.738,-9.74,-25.529,-9.738,-35.268,0
  20 + l-17.346,17.347 l53.199,53.196
  21 + L212.696,60.502z"/>
  22 + </g>
  23 + </svg>
  24 + }
  25 +}
  1 +const NS :&str = "http://www.w3.org/2000/svg";
  2 +
  3 +pub(crate) mod discard;
  4 +pub(crate) mod edit;
  5 +pub(crate) mod save;
  6 +pub(crate) mod select;
  7 +
  8 +pub(crate) use discard::discard_icon;
  9 +pub(crate) use edit::edit_icon;
  10 +pub(crate) use save::save_icon;
  11 +pub(crate) use select::select_icon;
  1 +use mogwai::prelude::*;
  2 +use super::NS;
  3 +
  4 +pub(crate) fn save_icon() -> ViewBuilder<Dom> {
  5 + builder! {
  6 + <svg xmlns=NS viewBox="0 -1 32 32"
  7 + style:width="1.5em" style:height="1.5em"
  8 + style:enable_background="new 0 0 32 32"
  9 + style:fill="none"
  10 + style:stroke="#000"
  11 + style:stroke_width="2"
  12 + style:stroke_linecap="round"
  13 + style:stroke_linejoin="round"
  14 + style:stroke_miterlimit="10">
  15 + <ellipse xmlns=NS
  16 + cx="14" cy="8" rx="10" ry="5"/>
  17 + <line xmlns=NS
  18 + x1="24" y1="16" x2="24" y2="8"/>
  19 + <path xmlns=NS
  20 + d="M4,8 v8 c0,2.8,4.5,5,10,5
  21 + c1.2,0,2.3-0.1,3.4-0.3"/>
  22 + <path xmlns=NS
  23 + d="M4,16 v8 c0,2.8,4.5,5,10,5
  24 + c2,0,3.8-0.3,5.3-0.8"/>
  25 + <circle xmlns=NS
  26 + cx="24" cy="23" r="7"/>
  27 + <line xmlns=NS
  28 + x1="24" y1="16" x2="24" y2="26"/>
  29 + <polyline xmlns=NS
  30 + points="21,23 24,26 27,23"/>
  31 + </svg>
  32 + }
  33 +}
  1 +use mogwai::prelude::*;
  2 +use super::NS;
  3 +
  4 +pub(crate) fn select_icon() -> ViewBuilder<Dom> {
  5 + builder! {
  6 + <svg xmlns=NS viewBox="0 -1 24 24"
  7 + style:width="1.5em" style:height="1.5em"
  8 + style:fill="none">
  9 + <path xmlns=NS
  10 + style:fill="black"
  11 + d="M9.17154 11.508
  12 + L7.75732 10.0938
  13 + L12 5.85113
  14 + L16.2426 10.0938
  15 + L14.8284 11.508
  16 + L12 8.67956
  17 + L9.17154 11.508Z"/>
  18 + <path xmlns=NS
  19 + style:fill="black"
  20 + d="M9.17154 12.492
  21 + L7.75732 13.9062
  22 + L12 18.1489
  23 + L16.2426 13.9062
  24 + L14.8284 12.492
  25 + L12 15.3204
  26 + L9.17154 12.492Z"/>
  27 + <path xmlns=NS
  28 + style:fill="black"
  29 + style:fill_rule="evenodd"
  30 + style:clip_rule="evenodd"
  31 + d="M1 5
  32 + C1 2.79086 2.79086 1 5 1
  33 + H19
  34 + C21.2091 1 23 2.79086 23 5
  35 + V19
  36 + C23 21.2091 21.2091 23 19 23
  37 + H5
  38 + C2.79086 23 1 21.2091 1 19
  39 + V5Z
  40 + M5 3
  41 + H19
  42 + C20.1046 3 21 3.89543 21 5
  43 + V19
  44 + C21 20.1046 20.1046 21 19 21
  45 + H5
  46 + C3.89543 21 3 20.1046 3 19
  47 + V5
  48 + C3 3.89543 3.89543 3 5 3Z"/>
  49 + </svg>
  50 + }
  51 +}
  1 +pub(crate) mod icons;
  1 +use std::fmt::Display;
  2 +use wasm_bindgen::prelude::*;
  3 +
  4 +type ParentError = Option<Box<dyn std::error::Error>>;
  5 +
  6 +#[derive(Debug)]
  7 +pub(crate) struct Error {
  8 + source: ParentError,
  9 + message: String,
  10 +}
  11 +
  12 +pub(crate) type Result<T> = std::result::Result<T, Error>;
  13 +
  14 +impl std::error::Error for Error {
  15 + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
  16 + self.source.as_deref()
  17 + }
  18 +}
  19 +
  20 +impl Display for Error {
  21 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  22 + match self {
  23 + Error { source: Some(source), message } =>
  24 + write!(f, "{}: {}", message, source),
  25 + Error { source: None, message } => write!(f, "{}", message),
  26 + }
  27 + }
  28 +}
  29 +
  30 +impl From<&str> for Error {
  31 + fn from(message: &str) -> Self {
  32 + Self { source: None, message: String::from(message) }
  33 + }
  34 +}
  35 +
  36 +impl From<JsValue> for Error {
  37 + fn from(source: JsValue) -> Self {
  38 + let source = js_sys::Error::from(source);
  39 +
  40 + let message = format!("[{}] {}", source.name(), source.message());
  41 + let source = Error::from(message.as_str());
  42 +
  43 + Self { source: Some(Box::new(source))
  44 + , message: String::from("WebSys Error")
  45 + }
  46 + }
  47 +}
  48 +
  49 +impl From<serde_json::Error> for Error {
  50 + fn from(source: serde_json::Error) -> Self {
  51 + Self { source: Some(Box::new(source))
  52 + , message: String::from("Serde Error")
  53 + }
  54 + }
  55 +}
  1 +mod api;
1 mod data; 2 mod data;
  3 +mod error;
  4 +mod client;
2 5
  6 +use api::markdown::Markdown;
  7 +use data::icons::*;
3 use log::Level; 8 use log::Level;
4 use mogwai::prelude::*; 9 use mogwai::prelude::*;
5 -use web_sys::{RequestInit, RequestMode, Request, Response, console};  
6 use std::panic; 10 use std::panic;
7 use wasm_bindgen::prelude::*; 11 use wasm_bindgen::prelude::*;
8 12
9 -  
10 #[derive(Clone)] 13 #[derive(Clone)]
11 enum AppLogic { 14 enum AppLogic {
12 Update, 15 Update,
13 Toggle, 16 Toggle,
14 -}  
15 -  
16 -fn md_to_html(source: &str) -> String {  
17 - use pulldown_cmark::{Parser, Options, html};  
18 -  
19 - let parser = Parser::new_ext(source, Options::all());  
20 - let mut html_output = String::new();  
21 - html::push_html(&mut html_output, parser);  
22 -  
23 - html_output 17 + Store,
  18 + Select,
  19 + Choose(Option<i32>),
  20 + Discard,
24 } 21 }
25 22
26 async fn editor_logic( mut rx_logic: broadcast::Receiver<AppLogic> 23 async fn editor_logic( mut rx_logic: broadcast::Receiver<AppLogic>
27 - , tx_view: broadcast::Sender<String>  
28 - , mut rx_dom: broadcast::Receiver<Dom> ) {  
29 - let window = web_sys::window().unwrap();  
30 - let mut opts = RequestInit::new();  
31 - opts.method("GET").mode(RequestMode::Cors);  
32 - let request = Request::new_with_str_and_init("/api/v0/users", &opts).unwrap();  
33 - request.headers().set("Accept", "application/json").unwrap();  
34 -  
35 - let response = JsFuture::from(window.fetch_with_request(&request))  
36 - . await.unwrap()  
37 - . dyn_into::<Response>().unwrap();  
38 - let data = JsFuture::from(response.json().unwrap()).await.unwrap();  
39 - console::log_1(&data);  
40 - 24 + , tx_logic: broadcast::Sender<AppLogic>
  25 + , tx_toggle: broadcast::Sender<bool>
  26 + , tx_patches: mpmc::Sender<ListPatch<ViewBuilder<Dom>>>
  27 + , mut rx_dom: broadcast::Receiver<Dom> )
  28 +{
41 let dom = rx_dom.next().await.unwrap(); 29 let dom = rx_dom.next().await.unwrap();
42 let mut show_edit = false; 30 let mut show_edit = false;
  31 + let mut md = Markdown::new("md-example").await.unwrap();
  32 +
  33 + let container = match dom.inner_read() {
  34 + Either::Left(dom_js) =>
  35 + Some( ( dom_js
  36 + . to_owned()
  37 + . dyn_into::<Node>().unwrap()
  38 + . first_child().unwrap()
  39 + . first_child().unwrap()
  40 + . dyn_into::<HtmlElement>().unwrap()
  41 + , dom_js
  42 + . to_owned()
  43 + . dyn_into::<Node>().unwrap()
  44 + . child_nodes().get(1).unwrap()
  45 + . child_nodes().get(1).unwrap()
  46 + . dyn_into::<HtmlElement>().unwrap() )),
  47 + _ => None,
  48 + };
  49 +
  50 + let cont_ref = container.as_ref();
  51 +
  52 + let get_md = move || {
  53 + match cont_ref {
  54 + Some((md_cont, _)) => md_cont.inner_text(),
  55 + None => String::from(""),
  56 + }
  57 + };
43 58
44 - fn get_md(dom: &Dom) -> String {  
45 - match dom.inner_read() {  
46 - Either::Left(dom_js) => dom_js . to_owned()  
47 - . dyn_into::<Node>().unwrap()  
48 - . first_child().unwrap()  
49 - . dyn_into::<HtmlElement>().unwrap()  
50 - . inner_text(),  
51 - _ => String::from(""), 59 + let set_md = move |md :&str| {
  60 + match cont_ref {
  61 + Some((md_cont, _)) => md_cont.set_text_content(Some(md)),
  62 + None => (),
52 } 63 }
53 - } 64 + };
54 65
55 - fn update(dom: &Dom) {  
56 - if let Either::Left(dom_js) = dom.inner_read() {  
57 - dom_js . to_owned()  
58 - . dyn_into::<Node>().unwrap()  
59 - . child_nodes().get(1).unwrap()  
60 - . child_nodes().get(1).unwrap()  
61 - . dyn_into::<HtmlElement>().unwrap()  
62 - . set_inner_html(md_to_html(get_md(dom).as_str()).as_str())  
63 - };  
64 - } 66 + let update = move || {
  67 + match cont_ref {
  68 + Some((_, view_cont)) => {
  69 + use pulldown_cmark::{Parser, Options, html};
  70 +
  71 + let mut html_out = String::new();
  72 + let md = get_md();
  73 + let parser = Parser::new_ext(&md, Options::all());
65 74
66 - update(&dom); 75 + html::push_html(&mut html_out, parser);
  76 + view_cont.set_inner_html(&html_out)
  77 + },
  78 + None => (),
  79 + }
  80 + };
  81 +
  82 + set_md(md.json.content.as_str());
  83 + update();
  84 +
  85 + /* play with katex ==== */
  86 + let opts = katex::Opts::builder()
  87 + . output_type(katex::opts::OutputType::Mathml)
  88 + . build().unwrap();
  89 + let formula1 = katex::render_with_opts("E = mc^2", &opts).unwrap();
  90 + let formula2 = katex::render_with_opts("e^{i*\\pi} +1 = 0", &opts).unwrap();
  91 +
  92 + if let Either::Left(dom_js) = dom.inner_read() {
  93 + dom_js . to_owned()
  94 + . dyn_into::<Node>().unwrap()
  95 + . child_nodes().get(1).unwrap()
  96 + . child_nodes().get(2).unwrap()
  97 + . dyn_into::<HtmlElement>().unwrap()
  98 + . set_inner_html(formula1.as_str())
  99 + };
  100 +
  101 + if let Either::Left(dom_js) = dom.inner_read() {
  102 + dom_js . to_owned()
  103 + . dyn_into::<Node>().unwrap()
  104 + . child_nodes().get(1).unwrap()
  105 + . child_nodes().get(3).unwrap()
  106 + . dyn_into::<HtmlElement>().unwrap()
  107 + . set_inner_html(formula2.as_str())
  108 + };
  109 + /* =========== */
67 110
68 while let Some(msg) = rx_logic.next().await { 111 while let Some(msg) = rx_logic.next().await {
69 match msg { 112 match msg {
70 - AppLogic::Update => update(&dom), 113 + AppLogic::Store => {
  114 + md.json.content = get_md();
  115 + md.save().await.unwrap();
  116 + },
  117 + AppLogic::Update => update(),
71 AppLogic::Toggle => { 118 AppLogic::Toggle => {
72 show_edit = ! show_edit; 119 show_edit = ! show_edit;
73 - match show_edit {  
74 - true => tx_view . broadcast(String::from("block"))  
75 - . await.unwrap(),  
76 - false => tx_view . broadcast(String::from("none"))  
77 - . await.unwrap(),  
78 - }; 120 + tx_toggle.broadcast(show_edit).await.unwrap();
  121 + },
  122 + AppLogic::Discard => {
  123 + set_md(md.json.content.as_str());
  124 + update();
  125 + },
  126 + AppLogic::Select => {
  127 + let patches = md
  128 + . patches().await.unwrap()
  129 + . into_iter()
  130 + . map(|diff| {
  131 + let id = Some(diff.id);
  132 + let choose_filter = tx_logic
  133 + . sink()
  134 + . contra_map(move |_| AppLogic::Choose(id));
  135 + builder! {
  136 + <li><button on:click=choose_filter
  137 + value=format!("{}", diff.id.to_owned())>
  138 + {diff.date_created.to_owned()}
  139 + </button></li>
  140 + }});
  141 + let all = vec![builder! {
  142 + <li><button on:click=tx_logic.sink().contra_map(|_| AppLogic::Choose(None))>
  143 + "Current"
  144 + </button></li>
  145 + }].into_iter().chain(patches);
  146 +
  147 + let list_replace = ListPatch::splice(.., all);
  148 + tx_patches.send(list_replace).await.unwrap();
  149 + },
  150 + AppLogic::Choose(id) => {
  151 + md.read(id).await.unwrap();
  152 + set_md(md.json.content.as_str());
  153 + update();
79 }, 154 },
80 } 155 }
81 } 156 }
82 } 157 }
83 158
84 fn editor_view( tx_logic: broadcast::Sender<AppLogic> 159 fn editor_view( tx_logic: broadcast::Sender<AppLogic>
85 - , rx_view: broadcast::Receiver<String> 160 + , rx_toggle: broadcast::Receiver<bool>
  161 + , rx_patches: mpmc::Receiver<ListPatch<ViewBuilder<Dom>>>
86 , tx_dom: broadcast::Sender<Dom> 162 , tx_dom: broadcast::Sender<Dom>
87 -) -> ViewBuilder<Dom> {  
88 - let ns = "http://www.w3.org/2000/svg"; 163 + ) -> ViewBuilder<Dom>
  164 +{
  165 + let input_filter = tx_logic
  166 + . sink()
  167 + . contra_map(|_| AppLogic::Update);
  168 + let store_filter = tx_logic
  169 + . sink()
  170 + . contra_map(|_| AppLogic::Store);
  171 + /* keep as example how to handle concrete events. ==
  172 + . contra_filter_map(|e :DomEvent| {
  173 + if let Either::Left(e) = e.clone_inner() {
  174 + let e = e.dyn_into::<MouseEvent>().unwrap();
  175 + match e.alt_key() {
  176 + true => Some(AppLogic::Store),
  177 + false => None
  178 + }
  179 + } else {
  180 + None
  181 + }
  182 + });
  183 + == */
  184 + let toggle_filter = tx_logic
  185 + . sink()
  186 + . contra_map(|_| AppLogic::Toggle);
  187 + let select_filter = tx_logic
  188 + . sink()
  189 + . contra_map(|_e| AppLogic::Select);
  190 + let discard_filter = tx_logic
  191 + . sink()
  192 + . contra_map(|_| AppLogic::Discard);
  193 +
  194 + let toggle_map = rx_toggle
  195 + . map(|t| match t {
  196 + true => String::from("block"),
  197 + false => String::from("none") });
  198 +
89 builder! { 199 builder! {
90 <div class="input" 200 <div class="input"
91 style:width="33%" 201 style:width="33%"
92 - on:input=tx_logic.sink().contra_map(|_| AppLogic::Update) 202 + on:input=input_filter
93 capture:view=tx_dom.sink()> 203 capture:view=tx_dom.sink()>
94 <div contenteditable="true" 204 <div contenteditable="true"
95 style:cursor="text" 205 style:cursor="text"
96 - style:display=("none", rx_view)>  
97 - <pre>{data::MD_EXAMPLE}</pre> 206 + style:display=("none", toggle_map)>
  207 + <pre></pre>
98 </div> 208 </div>
99 <div> 209 <div>
100 - <button on:click=tx_logic . sink()  
101 - . contra_map(|_| AppLogic::Toggle)>  
102 - <svg version="1.1" id="Capa_1" xmlns=ns  
103 - x="0px" y="0px" viewBox="0 0 220.001 220.001"  
104 - style:width="1.5em" style:height="1.5em">  
105 - <g xmlns=ns>  
106 - <polygon xmlns=ns points="0,220 59.34,213.86 6.143,160.661"></polygon>  
107 - <path xmlns=ns d="M132.018,34.787l53.197,53.197L69.568,203.631L16.37,  
108 - 150.434L132.018,34.787z M212.696,60.502c9.738-9.738,9.742-25.527,  
109 - 0-35.268l-17.93-17.93c-9.738-9.74-25.529-9.738-35.268,0l-17.346,  
110 - 17.347l53.199,53.196L212.696,60.502z"></path>  
111 - </g>  
112 - </svg>  
113 - </button> 210 + <div>
  211 + <div>
  212 + <button on:click=select_filter>{select_icon()}</button>
  213 + <ul patch:children=rx_patches>
  214 + </ul>
  215 + </div>
  216 + <button on:click=store_filter>{save_icon()}</button>
  217 + <button on:click=discard_filter>{discard_icon()}</button>
  218 + <button on:click=toggle_filter>{edit_icon()}</button>
  219 + </div>
  220 + <div></div>
  221 + <div></div>
114 <div></div> 222 <div></div>
115 </div> 223 </div>
116 </div> 224 </div>
@@ -118,15 +226,18 @@ fn editor_view( tx_logic: broadcast::Sender<AppLogic> @@ -118,15 +226,18 @@ fn editor_view( tx_logic: broadcast::Sender<AppLogic>
118 } 226 }
119 227
120 #[wasm_bindgen(start)] 228 #[wasm_bindgen(start)]
121 -pub fn main() -> Result<(), JsValue> { 229 +pub async fn main() -> Result<(), JsValue> {
122 panic::set_hook(Box::new(console_error_panic_hook::hook)); 230 panic::set_hook(Box::new(console_error_panic_hook::hook));
123 console_log::init_with_level(Level::Trace).unwrap(); 231 console_log::init_with_level(Level::Trace).unwrap();
124 232
125 let (tx_dom, rx_dom) = broadcast::bounded(1); 233 let (tx_dom, rx_dom) = broadcast::bounded(1);
126 let (tx_logic, rx_logic) = broadcast::bounded(1); 234 let (tx_logic, rx_logic) = broadcast::bounded(1);
127 - let (tx_view, rx_view) = broadcast::bounded(1);  
128 - let comp = Component::from( editor_view(tx_logic, rx_view, tx_dom) )  
129 - . with_logic( editor_logic(rx_logic, tx_view, rx_dom) ); 235 + let (tx_toggle, rx_toggle) = broadcast::bounded(1);
  236 + let (tx_patches, rx_patches) = mpmc::bounded(1);
  237 +
  238 + let view = editor_view(tx_logic.clone(), rx_toggle, rx_patches, tx_dom);
  239 + let logic = editor_logic(rx_logic, tx_logic, tx_toggle, tx_patches, rx_dom);
  240 + let comp = Component::from(view).with_logic(logic);
130 241
131 let page = Component::from(builder! {{comp}}); 242 let page = Component::from(builder! {{comp}});
132 page.build()?.run() 243 page.build()?.run()
Please register or login to post a comment