From 98331229283efc9499bdc9e8dacaccc2587e62b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Ci=C4=99=C5=BCarkiewicz?= Date: Sun, 10 May 2020 23:23:38 -0700 Subject: [PATCH] A lot of stuff working --- resources/style.css | 19 ++-- src/index.rs | 9 +- src/main.rs | 225 ++++++++++++++++++++++++++++++++----------- src/page.rs | 31 +++++- src/page/store/fs.rs | 7 +- 5 files changed, 214 insertions(+), 77 deletions(-) diff --git a/resources/style.css b/resources/style.css index a588be7..f7cdab6 100644 --- a/resources/style.css +++ b/resources/style.css @@ -1,14 +1,15 @@ -* { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - padding: 0; - margin: 0; +body { + max-width: 40em; + margin: 0 auto; } -body { - background-color: #baa4a4; - height: 100%; +.button-warning { + color: white; + background: rgb(223, 117, 20); +} +.button-green { + color: white; + background: rgb(28, 184, 65); } /* TOOD: make the whole form exactly fit the screen (including the buttons) */ diff --git a/src/index.rs b/src/index.rs index 91d5161..9f0d57d 100644 --- a/src/index.rs +++ b/src/index.rs @@ -8,7 +8,6 @@ use std::collections::{HashMap, HashSet}; #[derive(Default)] pub struct Index { - // tag -> page_ids page_ids_by_tag: HashMap>, tags_by_page_id: HashMap>, title_by_page_id: HashMap, @@ -135,11 +134,11 @@ impl Index { .entry(tag.clone()) .or_default() .insert(page.id().to_owned()); - self.tags_by_page_id - .insert(page.id().to_owned(), page.tags.clone()); - self.title_by_page_id - .insert(page.id().to_owned(), page.title.clone()); } + self.tags_by_page_id + .insert(page.id().to_owned(), page.tags.clone()); + self.title_by_page_id + .insert(page.id().to_owned(), page.title.clone()); } fn clean_data_for_page(&mut self, id: Id) { diff --git a/src/main.rs b/src/main.rs index e9bee8e..209562c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ //! tagwiki -use anyhow::{bail, Result}; +use anyhow::{bail, format_err, Result}; use log::info; use std::sync::Arc; use structopt::StructOpt; @@ -21,8 +21,8 @@ mod index; mod util; use horrorshow::helper::doctype; -use horrorshow::owned_html; use horrorshow::prelude::*; +use horrorshow::{box_html, owned_html}; #[derive(Debug)] struct RejectAnyhow(anyhow::Error); @@ -68,44 +68,89 @@ struct GetParams { #[derive(Deserialize, Debug)] struct PostForm { - body: String, + body: Option, id: Option, + _method: Option, } -fn render_html_page(page: impl RenderOnce) -> impl RenderOnce { - owned_html! { - : doctype::HTML; - head { - link(rel="stylesheet", media="all", href="/_style.css"); - } - body : page; +impl PostForm { + fn get_body(&self) -> Result<&str> { + self.body + .as_deref() + .ok_or_else(|| format_err!("Missing body")) } } -fn render_page_editing_view(page: &page::Parsed) -> impl RenderOnce { - let body = page.source_body.clone(); - let id = page.id().to_owned(); +fn render_html_page(body: impl RenderOnce) -> impl RenderOnce { owned_html! { - form(action=".", method="post") { - input(type="submit", value="Save"); - input(type="hidden", name="id", value=id); - textarea(name="body") { - : body + : doctype::HTML; + head { + link(rel="stylesheet",href="https://unpkg.com/purecss@2.0.1/build/pure-min.css",crossorigin="anonymous"); + link(rel="stylesheet",href="https://unpkg.com/purecss@2.0.1/build/grids-responsive-min.css"); + meta(name="viewport",content="width=device-width, initial-scale=1"); + link(rel="stylesheet", media="all", href="/_style.css"); + } + body { + : body + } + } +} + +fn render_page_editing_view(page: Option<&page::Parsed>) -> impl RenderOnce { + if let Some(page) = page.as_ref() { + let body = page.source_body.clone(); + let id = page.id().to_owned(); + (box_html! { + form(action=".", method="post") { + input(type="submit", value="Save", class="pure-button pure-button-primary"); + input(type="hidden", name="id", value=id); + textarea(name="body") { + : body + } + } + }) as Box + } else { + box_html! { + form(action=".", method="post") { + input(type="submit", value="Save", class="pure-button pure-button-primary"); + input(type="hidden", name="_method", value="put"); + textarea(name="body"); } } - } } fn render_page_view(page: &page::Parsed) -> impl RenderOnce { let page_html = page.html.clone(); let id = page.id().to_owned(); + let id_copy = id.clone(); owned_html! { - form(action=".", method="get") { - input(type="hidden", name="edit", value="true"); - input(type="hidden", name="id", value=id); - button(type="submit"){ - : "Edit" + div(class="pure-menu pure-menu-horizontal") { + form(action="..", method="get", class="pure-menu-item") { + button(type="submit", class="pure-button"){ + : "Up" + } + } + form(action="/", method="get", class="pure-menu-item") { + input(type="hidden", name="edit", value="true"); + button(type="submit", class="pure-button button-green"){ + : "New" + } + } + form(action=".", method="get", class="pure-menu-item") { + input(type="hidden", name="edit", value="true"); + input(type="hidden", name="id", value=id); + button(type="submit", class="pure-button pure-button-primary"){ + : "Edit" + } + } + form(action=".", method="post", class="pure-menu-item") { + input(type="hidden", name="edit", value="true"); + input(type="hidden", name="id", value=id_copy); + input(type="hidden", name="_method", value="delete"); + button(type="submit", class="pure-button button-warning",onclick="return confirm('Are you sure?');"){ + : "Delete" + } } } : Raw(page_html) @@ -114,6 +159,19 @@ fn render_page_view(page: &page::Parsed) -> impl RenderOnce { fn render_post_list(posts: impl Iterator + 'static) -> impl RenderOnce { owned_html! { + div(class="pure-menu pure-menu-horizontal") { + form(action="..", method="get", class="pure-menu-item") { + button(type="submit", class="pure-button"){ + : "Up" + } + } + form(action="/", method="get", class="pure-menu-item") { + input(type="hidden", name="edit", value="true"); + button(type="submit", class="pure-button button-green"){ + : "New" + } + } + } ul { @ for post in posts { li { @@ -143,8 +201,8 @@ async fn handle_style_css() -> std::result::Result, .status(200) .header(warp::http::header::CONTENT_TYPE, "text/css") .body( - include_str!("../resources/reset.css").to_string() - + include_str!("../resources/style.css"), + // include_str!("../resources/reset.css").to_string() + include_str!("../resources/style.css").to_string(), ) .expect("correct redirect")) } @@ -154,9 +212,20 @@ async fn handle_post_wrapped( path: FullPath, form: PostForm, ) -> Result, warp::Rejection> { - handle_post(state, path, form) - .await - .map_err(|e| warp::reject::custom(RejectAnyhow(e))) + if let Some("put") = form._method.as_deref() { + // workaround for not being able to use `method="put"` in html forms + handle_put(state, path, form) + .await + .map_err(|e| warp::reject::custom(RejectAnyhow(e))) + } else if let Some("delete") = form._method.as_deref() { + handle_delete(state, path, form) + .await + .map_err(|e| warp::reject::custom(RejectAnyhow(e))) + } else { + handle_post(state, path, form) + .await + .map_err(|e| warp::reject::custom(RejectAnyhow(e))) + } } async fn handle_post( @@ -167,8 +236,8 @@ async fn handle_post( let tags = path_to_tags(&path); let mut write = state.page_store.write().await; - let post_id = if let Some(id) = form.id { - id + let post_id = if let Some(id) = form.id.as_deref() { + id.to_owned() } else { let results = write.find(tags.as_slice()); match results.matching_pages.len() { @@ -177,9 +246,9 @@ async fn handle_post( _ => return Ok(Box::new(warp_temporary_redirect_to_get_method(".".into()))), } }; - let page = write.get(post_id.clone()).await?; + let page = write.get(post_id.to_owned()).await?; - let page = page.with_new_source_body(&get_rid_of_windows_newlines(form.body)); + let page = page.with_new_source_body(&get_rid_of_windows_newlines(form.get_body()?.to_owned())); write.put(&page).await?; @@ -187,29 +256,55 @@ async fn handle_post( "?id={}", post_id )))) +} - /* - match results.matching_pages.len() { - 1 => { - let page = write - .get(results.matching_pages[0].id.clone()) - .await - .map_err(|e| warp::reject::custom(RejectAnyhow(e)))?; +async fn handle_put_wrapped( + state: Arc, + path: FullPath, + form: PostForm, +) -> Result, warp::Rejection> { + handle_put(state, path, form) + .await + .map_err(|e| warp::reject::custom(RejectAnyhow(e))) +} +async fn handle_put( + state: Arc, + _path: FullPath, + form: PostForm, +) -> Result> { + let page = page::Parsed::new(&get_rid_of_windows_newlines(form.get_body()?.to_owned())); + let mut write = state.page_store.write().await; + write.put(&page).await?; - let page = page.with_new_source_body(&get_rid_of_windows_newlines(form.body)); + Ok(Box::new(warp_temporary_redirect_to_get_method(&format!( + "?id={}", + page.id() + )))) +} - write - .put(&page) - .await - .map_err(|e| warp::reject::custom(RejectAnyhow(e)))?; +async fn handle_delete_wrapped( + state: Arc, + path: FullPath, + form: PostForm, +) -> Result, warp::Rejection> { + handle_delete(state, path, form) + .await + .map_err(|e| warp::reject::custom(RejectAnyhow(e))) +} +async fn handle_delete( + state: Arc, + _path: FullPath, + query: PostForm, +) -> Result> { + let mut write = state.page_store.write().await; + let page = write + .get(query.id.ok_or_else(|| format_err!("Missing ID"))?) + .await?; + write.delete(page.id().to_owned()).await?; - Ok(Box::new(warp_temporary_redirect_after_post(".".into()))) - } - _ => { - // TODO: ERROR - Ok(Box::new(format!("Results: {:?}", results))) - } - }*/ + Ok(Box::new(warp_temporary_redirect_to_get_method(&format!( + ".", + )))) } // I wish this could be generic @@ -223,11 +318,11 @@ async fn handle_get_wrapped( .map_err(|e| warp::reject::custom(RejectAnyhow(e))) } -fn render_page(page: &page::Parsed, edit: bool) -> Box { +fn render_page(page: Option<&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 + Box::new(render_page_view(page.expect("always some"))) as Box } } @@ -242,9 +337,13 @@ async fn handle_get( 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, + Some(&page), query.edit.is_some(), )))); + } else if query.edit.is_some() { + return Ok(warp_reply_from_render(render_html_page(render_page( + None, true, + )))); } let results = read.find(tags.as_slice()); if results.matching_tags != tags { @@ -255,7 +354,7 @@ async fn handle_get( if results.matching_pages.len() == 1 { let page = read.get(results.matching_pages[0].id.clone()).await?; Ok(warp_reply_from_render(render_html_page(render_page( - &page, + Some(&page), query.edit.is_some(), )))) } else { @@ -280,11 +379,21 @@ async fn start(opts: &cli::Opts) -> Result<()> { .and(warp::query::()) .and(warp::get()) .and_then(handle_get_wrapped)) - .or(with_state(state) + .or(with_state(state.clone()) .and(warp::path::full()) .and(warp::post()) .and(warp::filters::body::form()) - .and_then(handle_post_wrapped)); + .and_then(handle_post_wrapped)) + .or(with_state(state.clone()) + .and(warp::path::full()) + .and(warp::delete()) + .and(warp::filters::body::form()) + .and_then(handle_delete_wrapped)) + .or(with_state(state) + .and(warp::path::full()) + .and(warp::put()) + .and(warp::filters::body::form()) + .and_then(handle_put_wrapped)); info!("Listening on port {}", opts.port); let _serve = warp::serve(handler).run(([127, 0, 0, 1], opts.port)).await; diff --git a/src/page.rs b/src/page.rs index 304f4e3..61c9b82 100644 --- a/src/page.rs +++ b/src/page.rs @@ -113,12 +113,38 @@ fn parse_tags(body: &str) -> Vec { .collect() } +fn parse_title(body: &str) -> String { + lazy_static! { + static ref RE: regex::Regex = + regex::Regex::new(r"#+[[:space:]]+(.*)").expect("correct regex"); + } + + let title = RE + .captures_iter(&body) + .map(|m| m.get(1).expect("a value").as_str().trim().to_string()) + .next() + .unwrap_or_else(|| "".to_string()); + if title == "" { + "Untitled".to_string() + } else { + title + } +} + impl Parsed { pub fn id(&self) -> IdRef { self.headers.id.as_str() } - fn from_full_source(source: Source) -> Parsed { + pub fn new(body: &str) -> Parsed { + let headers = Headers { + id: crate::util::random_string(16), + ..Headers::default() + }; + Self::from_headers_and_body(headers, body.to_owned()) + } + + pub fn from_full_source(source: Source) -> Parsed { let (headers, body) = split_headers_and_body(&source); let headers = Headers::parse(headers, &source); @@ -128,6 +154,7 @@ impl Parsed { fn from_headers_and_body(headers: Headers, body: String) -> Parsed { let source = headers.to_markdown_string() + &body; let parser = pulldown_cmark::Parser::new(&body); + let title = parse_title(&body); let mut html_output = String::new(); pulldown_cmark::html::push_html(&mut html_output, parser); @@ -139,7 +166,7 @@ impl Parsed { source_body: body, source: Source(source), tags, - title: "TODO".into(), + title, } } diff --git a/src/page/store/fs.rs b/src/page/store/fs.rs index 5a578fc..ac63e9e 100644 --- a/src/page/store/fs.rs +++ b/src/page/store/fs.rs @@ -35,6 +35,7 @@ impl FsStore { } fn title_to_new_rel_path(&self, title: &str) -> PathBuf { + let title = title.trim(); let mut last_char_was_alphanum = false; let mut path_str = String::new(); for ch in title.chars() { @@ -56,11 +57,11 @@ impl FsStore { let initial_title = path_str.clone(); let mut path = PathBuf::from(&initial_title); let mut i = 1; - while let Some(_) = self.path_to_page.get(&path) { + while let Some(_) = self.path_to_page.get(&path.with_extension("md")) { path = PathBuf::from(format!("{}-{}", &initial_title, i)); i += 1; } - path + path.with_extension("md") } fn try_reading_page_from_entry_res( @@ -102,7 +103,7 @@ impl FsStore { async fn write_page_to_file(&self, rel_path: &Path, page: &page::Parsed) -> Result<()> { let page = page.clone(); use std::io::Write; - let path = self.root_path.join(rel_path); + let path = self.root_path.join(rel_path).with_extension("md"); let tmp_path = path.with_extension(format!("md.tmp.{}", crate::util::random_string(8))); tokio::task::spawn_blocking(move || -> Result<()> {