A lot of stuff working

This commit is contained in:
Dawid Ciężarkiewicz 2020-05-10 23:23:38 -07:00
parent 1f899adee4
commit 9833122928
5 changed files with 214 additions and 77 deletions

View File

@ -1,14 +1,15 @@
* { body {
-webkit-box-sizing: border-box; max-width: 40em;
-moz-box-sizing: border-box; margin: 0 auto;
box-sizing: border-box;
padding: 0;
margin: 0;
} }
body { .button-warning {
background-color: #baa4a4; color: white;
height: 100%; 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) */ /* TOOD: make the whole form exactly fit the screen (including the buttons) */

View File

@ -8,7 +8,6 @@ use std::collections::{HashMap, HashSet};
#[derive(Default)] #[derive(Default)]
pub struct Index<T> { pub struct Index<T> {
// tag -> page_ids
page_ids_by_tag: HashMap<String, HashSet<Id>>, page_ids_by_tag: HashMap<String, HashSet<Id>>,
tags_by_page_id: HashMap<Id, Vec<Tag>>, tags_by_page_id: HashMap<Id, Vec<Tag>>,
title_by_page_id: HashMap<Id, String>, title_by_page_id: HashMap<Id, String>,
@ -135,11 +134,11 @@ impl<T> Index<T> {
.entry(tag.clone()) .entry(tag.clone())
.or_default() .or_default()
.insert(page.id().to_owned()); .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) { fn clean_data_for_page(&mut self, id: Id) {

View File

@ -1,6 +1,6 @@
//! tagwiki //! tagwiki
use anyhow::{bail, Result}; use anyhow::{bail, format_err, Result};
use log::info; use log::info;
use std::sync::Arc; use std::sync::Arc;
use structopt::StructOpt; use structopt::StructOpt;
@ -21,8 +21,8 @@ mod index;
mod util; mod util;
use horrorshow::helper::doctype; use horrorshow::helper::doctype;
use horrorshow::owned_html;
use horrorshow::prelude::*; use horrorshow::prelude::*;
use horrorshow::{box_html, owned_html};
#[derive(Debug)] #[derive(Debug)]
struct RejectAnyhow(anyhow::Error); struct RejectAnyhow(anyhow::Error);
@ -68,44 +68,89 @@ struct GetParams {
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
struct PostForm { struct PostForm {
body: String, body: Option<String>,
id: Option<String>, id: Option<String>,
_method: Option<String>,
} }
fn render_html_page(page: impl RenderOnce) -> impl RenderOnce { impl PostForm {
owned_html! { fn get_body(&self) -> Result<&str> {
: doctype::HTML; self.body
head { .as_deref()
link(rel="stylesheet", media="all", href="/_style.css"); .ok_or_else(|| format_err!("Missing body"))
}
body : page;
} }
} }
fn render_page_editing_view(page: &page::Parsed) -> impl RenderOnce { fn render_html_page(body: impl RenderOnce) -> impl RenderOnce {
let body = page.source_body.clone();
let id = page.id().to_owned();
owned_html! { owned_html! {
form(action=".", method="post") { : doctype::HTML;
input(type="submit", value="Save"); head {
input(type="hidden", name="id", value=id); link(rel="stylesheet",href="https://unpkg.com/purecss@2.0.1/build/pure-min.css",crossorigin="anonymous");
textarea(name="body") { link(rel="stylesheet",href="https://unpkg.com/purecss@2.0.1/build/grids-responsive-min.css");
: body 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<dyn RenderBox>
} 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 { fn render_page_view(page: &page::Parsed) -> impl RenderOnce {
let page_html = page.html.clone(); let page_html = page.html.clone();
let id = page.id().to_owned(); let id = page.id().to_owned();
let id_copy = id.clone();
owned_html! { owned_html! {
form(action=".", method="get") { div(class="pure-menu pure-menu-horizontal") {
input(type="hidden", name="edit", value="true"); form(action="..", method="get", class="pure-menu-item") {
input(type="hidden", name="id", value=id); button(type="submit", class="pure-button"){
button(type="submit"){ : "Up"
: "Edit" }
}
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) : Raw(page_html)
@ -114,6 +159,19 @@ fn render_page_view(page: &page::Parsed) -> impl RenderOnce {
fn render_post_list(posts: impl Iterator<Item = index::PageInfo> + 'static) -> impl RenderOnce { fn render_post_list(posts: impl Iterator<Item = index::PageInfo> + 'static) -> impl RenderOnce {
owned_html! { 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 { ul {
@ for post in posts { @ for post in posts {
li { li {
@ -143,8 +201,8 @@ async fn handle_style_css() -> std::result::Result<warp::http::Response<String>,
.status(200) .status(200)
.header(warp::http::header::CONTENT_TYPE, "text/css") .header(warp::http::header::CONTENT_TYPE, "text/css")
.body( .body(
include_str!("../resources/reset.css").to_string() // include_str!("../resources/reset.css").to_string()
+ include_str!("../resources/style.css"), include_str!("../resources/style.css").to_string(),
) )
.expect("correct redirect")) .expect("correct redirect"))
} }
@ -154,9 +212,20 @@ async fn handle_post_wrapped(
path: FullPath, path: FullPath,
form: PostForm, form: PostForm,
) -> Result<Box<dyn warp::Reply>, warp::Rejection> { ) -> Result<Box<dyn warp::Reply>, warp::Rejection> {
handle_post(state, path, form) if let Some("put") = form._method.as_deref() {
.await // workaround for not being able to use `method="put"` in html forms
.map_err(|e| warp::reject::custom(RejectAnyhow(e))) 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( async fn handle_post(
@ -167,8 +236,8 @@ async fn handle_post(
let tags = path_to_tags(&path); let tags = path_to_tags(&path);
let mut write = state.page_store.write().await; let mut write = state.page_store.write().await;
let post_id = if let Some(id) = form.id { let post_id = if let Some(id) = form.id.as_deref() {
id id.to_owned()
} else { } else {
let results = write.find(tags.as_slice()); let results = write.find(tags.as_slice());
match results.matching_pages.len() { match results.matching_pages.len() {
@ -177,9 +246,9 @@ async fn handle_post(
_ => return Ok(Box::new(warp_temporary_redirect_to_get_method(".".into()))), _ => 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?; write.put(&page).await?;
@ -187,29 +256,55 @@ async fn handle_post(
"?id={}", "?id={}",
post_id post_id
)))) ))))
}
/* async fn handle_put_wrapped(
match results.matching_pages.len() { state: Arc<State>,
1 => { path: FullPath,
let page = write form: PostForm,
.get(results.matching_pages[0].id.clone()) ) -> Result<Box<dyn warp::Reply>, warp::Rejection> {
.await handle_put(state, path, form)
.map_err(|e| warp::reject::custom(RejectAnyhow(e)))?; .await
.map_err(|e| warp::reject::custom(RejectAnyhow(e)))
}
async fn handle_put(
state: Arc<State>,
_path: FullPath,
form: PostForm,
) -> Result<Box<dyn warp::Reply>> {
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 async fn handle_delete_wrapped(
.put(&page) state: Arc<State>,
.await path: FullPath,
.map_err(|e| warp::reject::custom(RejectAnyhow(e)))?; form: PostForm,
) -> Result<Box<dyn warp::Reply>, warp::Rejection> {
handle_delete(state, path, form)
.await
.map_err(|e| warp::reject::custom(RejectAnyhow(e)))
}
async fn handle_delete(
state: Arc<State>,
_path: FullPath,
query: PostForm,
) -> Result<Box<dyn warp::Reply>> {
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()))) Ok(Box::new(warp_temporary_redirect_to_get_method(&format!(
} ".",
_ => { ))))
// TODO: ERROR
Ok(Box::new(format!("Results: {:?}", results)))
}
}*/
} }
// I wish this could be generic // I wish this could be generic
@ -223,11 +318,11 @@ async fn handle_get_wrapped(
.map_err(|e| warp::reject::custom(RejectAnyhow(e))) .map_err(|e| warp::reject::custom(RejectAnyhow(e)))
} }
fn render_page(page: &page::Parsed, edit: bool) -> Box<dyn RenderBox> { fn render_page(page: Option<&page::Parsed>, edit: bool) -> Box<dyn RenderBox> {
if edit { if edit {
Box::new(render_page_editing_view(page)) as Box<dyn RenderBox> Box::new(render_page_editing_view(page)) as Box<dyn RenderBox>
} else { } else {
Box::new(render_page_view(page)) as Box<dyn RenderBox> Box::new(render_page_view(page.expect("always some"))) as Box<dyn RenderBox>
} }
} }
@ -242,9 +337,13 @@ async fn handle_get(
if let Some(q_id) = query.id { if let Some(q_id) = query.id {
let page = read.get(q_id).await?; let page = read.get(q_id).await?;
return Ok(warp_reply_from_render(render_html_page(render_page( return Ok(warp_reply_from_render(render_html_page(render_page(
&page, Some(&page),
query.edit.is_some(), 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()); let results = read.find(tags.as_slice());
if results.matching_tags != tags { if results.matching_tags != tags {
@ -255,7 +354,7 @@ async fn handle_get(
if results.matching_pages.len() == 1 { if results.matching_pages.len() == 1 {
let page = read.get(results.matching_pages[0].id.clone()).await?; let page = read.get(results.matching_pages[0].id.clone()).await?;
Ok(warp_reply_from_render(render_html_page(render_page( Ok(warp_reply_from_render(render_html_page(render_page(
&page, Some(&page),
query.edit.is_some(), query.edit.is_some(),
)))) ))))
} else { } else {
@ -280,11 +379,21 @@ async fn start(opts: &cli::Opts) -> Result<()> {
.and(warp::query::<GetParams>()) .and(warp::query::<GetParams>())
.and(warp::get()) .and(warp::get())
.and_then(handle_get_wrapped)) .and_then(handle_get_wrapped))
.or(with_state(state) .or(with_state(state.clone())
.and(warp::path::full()) .and(warp::path::full())
.and(warp::post()) .and(warp::post())
.and(warp::filters::body::form()) .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); info!("Listening on port {}", opts.port);
let _serve = warp::serve(handler).run(([127, 0, 0, 1], opts.port)).await; let _serve = warp::serve(handler).run(([127, 0, 0, 1], opts.port)).await;

View File

@ -113,12 +113,38 @@ fn parse_tags(body: &str) -> Vec<String> {
.collect() .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 { impl Parsed {
pub fn id(&self) -> IdRef { pub fn id(&self) -> IdRef {
self.headers.id.as_str() 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, body) = split_headers_and_body(&source);
let headers = Headers::parse(headers, &source); let headers = Headers::parse(headers, &source);
@ -128,6 +154,7 @@ impl Parsed {
fn from_headers_and_body(headers: Headers, body: String) -> Parsed { fn from_headers_and_body(headers: Headers, body: String) -> Parsed {
let source = headers.to_markdown_string() + &body; let source = headers.to_markdown_string() + &body;
let parser = pulldown_cmark::Parser::new(&body); let parser = pulldown_cmark::Parser::new(&body);
let title = parse_title(&body);
let mut html_output = String::new(); let mut html_output = String::new();
pulldown_cmark::html::push_html(&mut html_output, parser); pulldown_cmark::html::push_html(&mut html_output, parser);
@ -139,7 +166,7 @@ impl Parsed {
source_body: body, source_body: body,
source: Source(source), source: Source(source),
tags, tags,
title: "TODO".into(), title,
} }
} }

View File

@ -35,6 +35,7 @@ impl FsStore {
} }
fn title_to_new_rel_path(&self, title: &str) -> PathBuf { fn title_to_new_rel_path(&self, title: &str) -> PathBuf {
let title = title.trim();
let mut last_char_was_alphanum = false; let mut last_char_was_alphanum = false;
let mut path_str = String::new(); let mut path_str = String::new();
for ch in title.chars() { for ch in title.chars() {
@ -56,11 +57,11 @@ impl FsStore {
let initial_title = path_str.clone(); let initial_title = path_str.clone();
let mut path = PathBuf::from(&initial_title); let mut path = PathBuf::from(&initial_title);
let mut i = 1; 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)); path = PathBuf::from(format!("{}-{}", &initial_title, i));
i += 1; i += 1;
} }
path path.with_extension("md")
} }
fn try_reading_page_from_entry_res( 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<()> { async fn write_page_to_file(&self, rel_path: &Path, page: &page::Parsed) -> Result<()> {
let page = page.clone(); let page = page.clone();
use std::io::Write; 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))); let tmp_path = path.with_extension(format!("md.tmp.{}", crate::util::random_string(8)));
tokio::task::spawn_blocking(move || -> Result<()> { tokio::task::spawn_blocking(move || -> Result<()> {