From 1f899adee47af9c5737242b3baef95a25b05d42b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Ci=C4=99=C5=BCarkiewicz?= Date: Sun, 10 May 2020 17:54:21 -0700 Subject: [PATCH] Progress --- Cargo.lock | 7 ++ Cargo.toml | 1 + resources/reset.css | 48 ++++++++++++ resources/style.css | 18 +++++ src/index.rs | 46 +++++++++-- src/main.rs | 187 ++++++++++++++++++++++++++++++++++++-------- src/page.rs | 7 +- 7 files changed, 273 insertions(+), 41 deletions(-) create mode 100644 resources/reset.css create mode 100644 resources/style.css diff --git a/Cargo.lock b/Cargo.lock index 91f8e0e..b9e579f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -424,6 +424,12 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "644f9158b2f133fd50f5fb3242878846d9eb792e445c893805ff0e3824006e35" +[[package]] +name = "horrorshow" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ce7e0a1bc8e4489896abc94e5664e811a502a151bebfe113b3214fa181d3fb" + [[package]] name = "http" version = "0.2.1" @@ -1194,6 +1200,7 @@ dependencies = [ "digest", "env_logger", "hex", + "horrorshow", "lazy_static", "log 0.4.8", "pulldown-cmark", diff --git a/Cargo.toml b/Cargo.toml index d9a4a84..6b1060b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,3 +32,4 @@ walkdir = "*" async-trait = "0.1.30" serde = "*" serde_derive = "*" +horrorshow = "*" diff --git a/resources/reset.css b/resources/reset.css new file mode 100644 index 0000000..e29c0f5 --- /dev/null +++ b/resources/reset.css @@ -0,0 +1,48 @@ +/* http://meyerweb.com/eric/tools/css/reset/ + v2.0 | 20110126 + License: none (public domain) +*/ + +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} +/* HTML5 display-role reset for older browsers */ +article, aside, details, figcaption, figure, +footer, header, hgroup, menu, nav, section { + display: block; +} +body { + line-height: 1; +} +ol, ul { + list-style: none; +} +blockquote, q { + quotes: none; +} +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; +} +table { + border-collapse: collapse; + border-spacing: 0; +} diff --git a/resources/style.css b/resources/style.css new file mode 100644 index 0000000..a588be7 --- /dev/null +++ b/resources/style.css @@ -0,0 +1,18 @@ +* { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + padding: 0; + margin: 0; +} + +body { + background-color: #baa4a4; + height: 100%; +} + +/* TOOD: make the whole form exactly fit the screen (including the buttons) */ +textarea { + height: 90vh; + width: 100%; +} diff --git a/src/index.rs b/src/index.rs index 4524948..91d5161 100644 --- a/src/index.rs +++ b/src/index.rs @@ -11,12 +11,19 @@ pub struct Index { // tag -> page_ids page_ids_by_tag: HashMap>, tags_by_page_id: HashMap>, + title_by_page_id: HashMap, store: T, } +#[derive(Debug, Clone)] +pub struct PageInfo { + pub id: Id, + pub title: String, +} + #[derive(Default, Debug, Clone)] pub struct FindResults { - pub matching_pages: Vec, + pub matching_pages: Vec, pub matching_tags: Vec, } @@ -34,6 +41,7 @@ where let mut s = Index { page_ids_by_tag: Default::default(), tags_by_page_id: Default::default(), + title_by_page_id: Default::default(), store, }; @@ -57,9 +65,22 @@ where impl Index { pub fn find(&self, tags: &[TagRef]) -> FindResults { - let mut matching_pages: Vec = vec![]; + let mut matching_pages: Vec = vec![]; let mut matching_tags: Vec = vec![]; let mut already_tried_tags = HashSet::new(); + + if tags.is_empty() { + matching_pages = self + .tags_by_page_id + .keys() + .cloned() + .map(|id| PageInfo { + id: id.clone(), + title: self.title_by_page_id[&id].clone(), + }) + .collect(); + } + for tag in tags { if already_tried_tags.contains(tag) { continue; @@ -67,7 +88,13 @@ impl Index { already_tried_tags.insert(tag); if matching_tags.is_empty() { if let Some(ids) = &self.page_ids_by_tag.get(*tag) { - matching_pages = ids.iter().map(|id| id.to_owned()).collect(); + matching_pages = ids + .iter() + .map(|id| PageInfo { + id: id.to_owned(), + title: self.title_by_page_id[id].clone(), + }) + .collect(); matching_tags.push(tag.to_string()) } else { return FindResults::empty(); @@ -76,7 +103,7 @@ impl Index { if let Some(ids) = self.page_ids_by_tag.get(*tag) { let new_matching_pages: Vec<_> = matching_pages .iter() - .filter(|id| ids.contains(id.as_str())) + .filter(|info| ids.contains(info.id.as_str())) .map(|id| id.to_owned()) .collect(); if new_matching_pages.is_empty() { @@ -107,9 +134,11 @@ impl Index { self.page_ids_by_tag .entry(tag.clone()) .or_default() - .insert(page.headers.id.clone()); + .insert(page.id().to_owned()); self.tags_by_page_id - .insert(page.headers.id.clone(), page.tags.clone()); + .insert(page.id().to_owned(), page.tags.clone()); + self.title_by_page_id + .insert(page.id().to_owned(), page.title.clone()); } } @@ -125,6 +154,7 @@ impl Index { .map(|set| set.remove(&id)); } self.tags_by_page_id.remove(&id); + self.title_by_page_id.remove(&id); } } @@ -140,8 +170,8 @@ where async fn put(&mut self, page: &page::Parsed) -> Result<()> { self.store.put(page).await?; - if let Some(_tags) = self.tags_by_page_id.get(&page.headers.id) { - self.clean_data_for_page(page.headers.id.clone()); + if let Some(_tags) = self.tags_by_page_id.get(page.id()) { + self.clean_data_for_page(page.id().to_owned()); } self.add_data_for_page(page); diff --git a/src/main.rs b/src/main.rs index 03709d9..e9bee8e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ //! tagwiki -use anyhow::Result; +use anyhow::{bail, Result}; use log::info; use std::sync::Arc; use structopt::StructOpt; @@ -20,6 +20,10 @@ mod index; /// Utils mod util; +use horrorshow::helper::doctype; +use horrorshow::owned_html; +use horrorshow::prelude::*; + #[derive(Debug)] struct RejectAnyhow(anyhow::Error); @@ -44,7 +48,7 @@ fn warp_temporary_redirect(location: &str) -> warp::http::Response<&'static str> .expect("correct redirect") } -fn warp_temporary_redirect_after_post(location: &str) -> warp::http::Response<&'static str> { +fn warp_temporary_redirect_to_get_method(location: &str) -> warp::http::Response<&'static str> { warp::http::Response::builder() .status(303) .header(warp::http::header::LOCATION, location) @@ -57,20 +61,73 @@ fn get_rid_of_windows_newlines(s: String) -> String { } #[derive(Deserialize, Debug)] -struct GetPrompt { +struct GetParams { edit: Option, + id: Option, } #[derive(Deserialize, Debug)] struct PostForm { body: String, + id: Option, } -fn html_for_editing_page(page: &page::Parsed) -> String { - format!( - "

", - page.source_body - ) +fn render_html_page(page: impl RenderOnce) -> impl RenderOnce { + owned_html! { + : doctype::HTML; + head { + link(rel="stylesheet", media="all", href="/_style.css"); + } + body : page; + } +} + +fn render_page_editing_view(page: &page::Parsed) -> impl RenderOnce { + let body = page.source_body.clone(); + let id = page.id().to_owned(); + owned_html! { + form(action=".", method="post") { + input(type="submit", value="Save"); + input(type="hidden", name="id", value=id); + textarea(name="body") { + : body + } + } + + } +} + +fn render_page_view(page: &page::Parsed) -> impl RenderOnce { + let page_html = page.html.clone(); + let id = page.id().to_owned(); + owned_html! { + form(action=".", method="get") { + input(type="hidden", name="edit", value="true"); + input(type="hidden", name="id", value=id); + button(type="submit"){ + : "Edit" + } + } + : Raw(page_html) + } +} + +fn render_post_list(posts: impl Iterator + 'static) -> impl RenderOnce { + owned_html! { + ul { + @ for post in posts { + li { + a(href=format!("?id={}", post.id)) : post.title + } + } + } + } +} + +fn warp_reply_from_render(render: impl RenderOnce) -> Box { + Box::new(warp::reply::html( + render.into_string().expect("rendering without errors"), + )) } fn path_to_tags(path: &FullPath) -> Vec<&str> { @@ -81,19 +138,61 @@ fn path_to_tags(path: &FullPath) -> Vec<&str> { .collect() } +async fn handle_style_css() -> std::result::Result, warp::Rejection> { + Ok(warp::http::Response::builder() + .status(200) + .header(warp::http::header::CONTENT_TYPE, "text/css") + .body( + include_str!("../resources/reset.css").to_string() + + include_str!("../resources/style.css"), + ) + .expect("correct redirect")) +} + +async fn handle_post_wrapped( + state: Arc, + path: FullPath, + form: PostForm, +) -> Result, warp::Rejection> { + handle_post(state, path, form) + .await + .map_err(|e| warp::reject::custom(RejectAnyhow(e))) +} + async fn handle_post( state: Arc, path: FullPath, form: PostForm, -) -> std::result::Result, warp::Rejection> { +) -> Result> { let tags = path_to_tags(&path); let mut write = state.page_store.write().await; - let results = write.find(tags.as_slice()); + let post_id = if let Some(id) = form.id { + id + } else { + let results = write.find(tags.as_slice()); + match results.matching_pages.len() { + 1 => results.matching_pages[0].id.clone(), + 0 => bail!("Page not found"), + _ => return Ok(Box::new(warp_temporary_redirect_to_get_method(".".into()))), + } + }; + let page = write.get(post_id.clone()).await?; + + let page = page.with_new_source_body(&get_rid_of_windows_newlines(form.body)); + + write.put(&page).await?; + + Ok(Box::new(warp_temporary_redirect_to_get_method(&format!( + "?id={}", + post_id + )))) + + /* match results.matching_pages.len() { 1 => { let page = write - .get(results.matching_pages[0].clone()) + .get(results.matching_pages[0].id.clone()) .await .map_err(|e| warp::reject::custom(RejectAnyhow(e)))?; @@ -110,16 +209,43 @@ async fn handle_post( // TODO: ERROR Ok(Box::new(format!("Results: {:?}", results))) } + }*/ +} + +// I wish this could be generic +async fn handle_get_wrapped( + state: Arc, + path: FullPath, + query: GetParams, +) -> std::result::Result, warp::Rejection> { + handle_get(state, path, query) + .await + .map_err(|e| warp::reject::custom(RejectAnyhow(e))) +} + +fn render_page(page: &page::Parsed, edit: bool) -> Box { + if edit { + Box::new(render_page_editing_view(page)) as Box + } else { + Box::new(render_page_view(page)) as Box } } async fn handle_get( state: Arc, path: FullPath, - query: GetPrompt, -) -> std::result::Result, warp::Rejection> { + query: GetParams, +) -> Result> { let tags = path_to_tags(&path); let read = state.page_store.read().await; + + if let Some(q_id) = query.id { + let page = read.get(q_id).await?; + return Ok(warp_reply_from_render(render_html_page(render_page( + &page, + query.edit.is_some(), + )))); + } let results = read.find(tags.as_slice()); if results.matching_tags != tags { return Ok(Box::new(warp_temporary_redirect( @@ -127,18 +253,15 @@ async fn handle_get( ))); } if results.matching_pages.len() == 1 { - let page = read - .get(results.matching_pages[0].clone()) - .await - .map_err(|e| warp::reject::custom(RejectAnyhow(e)))?; - Ok(Box::new(warp::reply::html(if query.edit.is_none() { - page.html - + "