mirror of https://github.com/dpc/tagwiki
Progress
This commit is contained in:
parent
94d3101cd1
commit
1f899adee4
|
@ -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",
|
||||
|
|
|
@ -32,3 +32,4 @@ walkdir = "*"
|
|||
async-trait = "0.1.30"
|
||||
serde = "*"
|
||||
serde_derive = "*"
|
||||
horrorshow = "*"
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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%;
|
||||
}
|
46
src/index.rs
46
src/index.rs
|
@ -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);
|
||||
|
|
187
src/main.rs
187
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<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;
|
||||
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue