This commit is contained in:
Dawid Ciężarkiewicz 2020-05-10 17:54:21 -07:00
parent 94d3101cd1
commit 1f899adee4
7 changed files with 273 additions and 41 deletions

7
Cargo.lock generated
View File

@ -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",

View File

@ -32,3 +32,4 @@ walkdir = "*"
async-trait = "0.1.30"
serde = "*"
serde_derive = "*"
horrorshow = "*"

48
resources/reset.css Normal file
View File

@ -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;
}

18
resources/style.css Normal file
View File

@ -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%;
}

View File

@ -11,12 +11,19 @@ pub struct Index<T> {
// tag -> page_ids
page_ids_by_tag: HashMap<String, HashSet<Id>>,
tags_by_page_id: HashMap<Id, Vec<Tag>>,
title_by_page_id: HashMap<Id, String>,
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<Id>,
pub matching_pages: Vec<PageInfo>,
pub matching_tags: Vec<page::Tag>,
}
@ -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<T> Index<T> {
pub fn find(&self, tags: &[TagRef]) -> FindResults {
let mut matching_pages: Vec<String> = vec![];
let mut matching_pages: Vec<PageInfo> = vec![];
let mut matching_tags: Vec<String> = 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<T> Index<T> {
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<T> Index<T> {
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<T> Index<T> {
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<T> Index<T> {
.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);

View File

@ -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<bool>,
id: Option<String>,
}
#[derive(Deserialize, Debug)]
struct PostForm {
body: String,
id: Option<String>,
}
fn html_for_editing_page(page: &page::Parsed) -> String {
format!(
"<form action='.' method='POST'><textarea name='body'>{}</textarea><br/><input type=submit></form>",
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<Item = index::PageInfo> + '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<dyn warp::Reply> {
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::http::Response<String>, 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<State>,
path: FullPath,
form: PostForm,
) -> Result<Box<dyn warp::Reply>, warp::Rejection> {
handle_post(state, path, form)
.await
.map_err(|e| warp::reject::custom(RejectAnyhow(e)))
}
async fn handle_post(
state: Arc<State>,
path: FullPath,
form: PostForm,
) -> std::result::Result<Box<dyn warp::Reply>, warp::Rejection> {
) -> Result<Box<dyn warp::Reply>> {
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<State>,
path: FullPath,
query: GetParams,
) -> std::result::Result<Box<dyn warp::Reply>, 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<dyn RenderBox> {
if edit {
Box::new(render_page_editing_view(page)) as Box<dyn RenderBox>
} else {
Box::new(render_page_view(page)) as Box<dyn RenderBox>
}
}
async fn handle_get(
state: Arc<State>,
path: FullPath,
query: GetPrompt,
) -> std::result::Result<Box<dyn warp::Reply>, warp::Rejection> {
query: GetParams,
) -> Result<Box<dyn warp::Reply>> {
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
+ "<form action='.' method='get'><input type='hidden' name='edit' value='true' /><button type='submit'/>Edit Page</form>"
} else {
html_for_editing_page(&page)
})))
let page = read.get(results.matching_pages[0].id.clone()).await?;
Ok(warp_reply_from_render(render_html_page(render_page(
&page,
query.edit.is_some(),
))))
} else {
Ok(Box::new(format!("Results: {:?}", results)))
Ok(warp_reply_from_render(render_html_page(render_post_list(
results.matching_pages.into_iter(),
))))
}
}
@ -151,17 +274,17 @@ async fn start(opts: &cli::Opts) -> Result<()> {
)),
});
let handler = warp::any()
.and(with_state(state.clone()))
.and(warp::path::full())
.and(warp::query::<GetPrompt>())
.and(warp::get())
.and_then(handle_get)
.or(warp::any()
.and(with_state(state))
.and(warp::path!("_style.css").and_then(handle_style_css))
.or(with_state(state.clone())
.and(warp::path::full())
.and(warp::query::<GetParams>())
.and(warp::get())
.and_then(handle_get_wrapped))
.or(with_state(state)
.and(warp::path::full())
.and(warp::post())
.and(warp::filters::body::form())
.and_then(handle_post));
.and_then(handle_post_wrapped));
info!("Listening on port {}", opts.port);
let _serve = warp::serve(handler).run(([127, 0, 0, 1], opts.port)).await;

View File

@ -10,6 +10,7 @@ use digest::Digest;
pub type Id = String;
pub type Tag = String;
pub type TagRef<'a> = &'a str;
pub type IdRef<'a> = &'a str;
const TAGWIKI_PAGE_ID_KEY: &str = "tagwiki-page-id";
@ -113,6 +114,10 @@ fn parse_tags(body: &str) -> Vec<String> {
}
impl Parsed {
pub fn id(&self) -> IdRef {
self.headers.id.as_str()
}
fn from_full_source(source: Source) -> Parsed {
let (headers, body) = split_headers_and_body(&source);
let headers = Headers::parse(headers, &source);
@ -186,6 +191,6 @@ tagwiki-id: 123
));
println!("{:#?}", page);
assert_eq!(page.headers.id, "xyz");
assert_eq!(page.id(), "xyz");
Ok(())
}