mirror of
https://github.com/dpc/tagwiki
synced 2025-04-15 15:23:12 +00:00
A lot of stuff working
This commit is contained in:
parent
1f899adee4
commit
9833122928
@ -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) */
|
||||
|
@ -8,7 +8,6 @@ use std::collections::{HashMap, HashSet};
|
||||
|
||||
#[derive(Default)]
|
||||
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>,
|
||||
@ -135,11 +134,11 @@ impl<T> Index<T> {
|
||||
.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) {
|
||||
|
225
src/main.rs
225
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<String>,
|
||||
id: Option<String>,
|
||||
_method: Option<String>,
|
||||
}
|
||||
|
||||
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<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 {
|
||||
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<Item = index::PageInfo> + '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<warp::http::Response<String>,
|
||||
.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<Box<dyn warp::Reply>, 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<State>,
|
||||
path: FullPath,
|
||||
form: PostForm,
|
||||
) -> Result<Box<dyn warp::Reply>, warp::Rejection> {
|
||||
handle_put(state, path, form)
|
||||
.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
|
||||
.put(&page)
|
||||
.await
|
||||
.map_err(|e| warp::reject::custom(RejectAnyhow(e)))?;
|
||||
async fn handle_delete_wrapped(
|
||||
state: Arc<State>,
|
||||
path: FullPath,
|
||||
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())))
|
||||
}
|
||||
_ => {
|
||||
// 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<dyn RenderBox> {
|
||||
fn render_page(page: Option<&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>
|
||||
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 {
|
||||
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::<GetParams>())
|
||||
.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;
|
||||
|
||||
|
31
src/page.rs
31
src/page.rs
@ -113,12 +113,38 @@ fn parse_tags(body: &str) -> Vec<String> {
|
||||
.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,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<()> {
|
||||
|
Loading…
x
Reference in New Issue
Block a user