mirror of
https://github.com/dpc/tagwiki
synced 2024-11-15 21:24:50 +00:00
Some stuff kind of working
This commit is contained in:
parent
e556d733b1
commit
94d3101cd1
13
Cargo.lock
generated
13
Cargo.lock
generated
@ -1062,6 +1062,17 @@ version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "36df6ac6412072f67cf767ebbde4133a5b2e88e76dc6187fa7104cd16f783399"
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10be45e22e5597d4b88afcc71f9d7bfadcd604bf0c78a3ab4582b8d2b37f39f3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.52"
|
||||
@ -1188,6 +1199,8 @@ dependencies = [
|
||||
"pulldown-cmark",
|
||||
"rand 0.6.5",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"structopt",
|
||||
"tokio",
|
||||
"walkdir",
|
||||
|
@ -30,3 +30,5 @@ digest = "*"
|
||||
hex = "*"
|
||||
walkdir = "*"
|
||||
async-trait = "0.1.30"
|
||||
serde = "*"
|
||||
serde_derive = "*"
|
||||
|
@ -59,9 +59,14 @@ impl<T> Index<T> {
|
||||
pub fn find(&self, tags: &[TagRef]) -> FindResults {
|
||||
let mut matching_pages: Vec<String> = vec![];
|
||||
let mut matching_tags: Vec<String> = vec![];
|
||||
let mut already_tried_tags = HashSet::new();
|
||||
for tag in tags {
|
||||
if already_tried_tags.contains(tag) {
|
||||
continue;
|
||||
}
|
||||
already_tried_tags.insert(tag);
|
||||
if matching_tags.is_empty() {
|
||||
if let Some(ids) = dbg!(&self.page_ids_by_tag).get(*tag) {
|
||||
if let Some(ids) = &self.page_ids_by_tag.get(*tag) {
|
||||
matching_pages = ids.iter().map(|id| id.to_owned()).collect();
|
||||
matching_tags.push(tag.to_string())
|
||||
} else {
|
||||
@ -98,7 +103,7 @@ impl<T> Index<T> {
|
||||
}
|
||||
|
||||
fn add_data_for_page(&mut self, page: &page::Parsed) {
|
||||
for tag in dbg!(&page.tags) {
|
||||
for tag in &page.tags {
|
||||
self.page_ids_by_tag
|
||||
.entry(tag.clone())
|
||||
.or_default()
|
||||
|
115
src/main.rs
115
src/main.rs
@ -6,6 +6,8 @@ use std::sync::Arc;
|
||||
use structopt::StructOpt;
|
||||
use warp::{path::FullPath, Filter};
|
||||
|
||||
use serde_derive::Deserialize;
|
||||
|
||||
use page::StoreMut;
|
||||
|
||||
/// Command line options
|
||||
@ -34,24 +36,107 @@ fn with_state(
|
||||
warp::any().map(move || state.clone())
|
||||
}
|
||||
|
||||
async fn handler(
|
||||
state: Arc<State>,
|
||||
path: FullPath,
|
||||
) -> std::result::Result<Box<dyn warp::Reply>, warp::Rejection> {
|
||||
let tags: Vec<_> = path
|
||||
.as_str()
|
||||
fn warp_temporary_redirect(location: &str) -> warp::http::Response<&'static str> {
|
||||
warp::http::Response::builder()
|
||||
.status(307)
|
||||
.header(warp::http::header::LOCATION, location)
|
||||
.body("")
|
||||
.expect("correct redirect")
|
||||
}
|
||||
|
||||
fn warp_temporary_redirect_after_post(location: &str) -> warp::http::Response<&'static str> {
|
||||
warp::http::Response::builder()
|
||||
.status(303)
|
||||
.header(warp::http::header::LOCATION, location)
|
||||
.body("")
|
||||
.expect("correct redirect")
|
||||
}
|
||||
|
||||
fn get_rid_of_windows_newlines(s: String) -> String {
|
||||
s.chars().filter(|ch| *ch != '\r').collect()
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct GetPrompt {
|
||||
edit: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct PostForm {
|
||||
body: 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 path_to_tags(path: &FullPath) -> Vec<&str> {
|
||||
path.as_str()
|
||||
.split('/')
|
||||
.map(|t| t.trim())
|
||||
.filter(|t| t != &"")
|
||||
.collect();
|
||||
.collect()
|
||||
}
|
||||
|
||||
async fn handle_post(
|
||||
state: Arc<State>,
|
||||
path: FullPath,
|
||||
form: PostForm,
|
||||
) -> std::result::Result<Box<dyn warp::Reply>, warp::Rejection> {
|
||||
let tags = path_to_tags(&path);
|
||||
let mut write = state.page_store.write().await;
|
||||
let results = write.find(tags.as_slice());
|
||||
|
||||
match results.matching_pages.len() {
|
||||
1 => {
|
||||
let page = write
|
||||
.get(results.matching_pages[0].clone())
|
||||
.await
|
||||
.map_err(|e| warp::reject::custom(RejectAnyhow(e)))?;
|
||||
|
||||
let page = page.with_new_source_body(&get_rid_of_windows_newlines(form.body));
|
||||
|
||||
write
|
||||
.put(&page)
|
||||
.await
|
||||
.map_err(|e| warp::reject::custom(RejectAnyhow(e)))?;
|
||||
|
||||
Ok(Box::new(warp_temporary_redirect_after_post(".".into())))
|
||||
}
|
||||
_ => {
|
||||
// TODO: ERROR
|
||||
Ok(Box::new(format!("Results: {:?}", results)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_get(
|
||||
state: Arc<State>,
|
||||
path: FullPath,
|
||||
query: GetPrompt,
|
||||
) -> std::result::Result<Box<dyn warp::Reply>, warp::Rejection> {
|
||||
let tags = path_to_tags(&path);
|
||||
let read = state.page_store.read().await;
|
||||
let results = read.find(tags.as_slice());
|
||||
if results.matching_tags != tags {
|
||||
return Ok(Box::new(warp_temporary_redirect(
|
||||
&("/".to_string() + &results.matching_tags.join("/")),
|
||||
)));
|
||||
}
|
||||
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(page.html)))
|
||||
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)
|
||||
})))
|
||||
} else {
|
||||
Ok(Box::new(format!("Results: {:?}", results)))
|
||||
}
|
||||
@ -66,9 +151,17 @@ async fn start(opts: &cli::Opts) -> Result<()> {
|
||||
)),
|
||||
});
|
||||
let handler = warp::any()
|
||||
.and(with_state(state))
|
||||
.and(with_state(state.clone()))
|
||||
.and(warp::path::full())
|
||||
.and_then(handler);
|
||||
.and(warp::query::<GetPrompt>())
|
||||
.and(warp::get())
|
||||
.and_then(handle_get)
|
||||
.or(warp::any()
|
||||
.and(with_state(state))
|
||||
.and(warp::path::full())
|
||||
.and(warp::post())
|
||||
.and(warp::filters::body::form())
|
||||
.and_then(handle_post));
|
||||
info!("Listening on port {}", opts.port);
|
||||
let _serve = warp::serve(handler).run(([127, 0, 0, 1], opts.port)).await;
|
||||
|
||||
@ -81,7 +174,7 @@ fn main() -> Result<()> {
|
||||
|
||||
tokio::runtime::Runtime::new()
|
||||
.unwrap()
|
||||
.block_on(start(&opts));
|
||||
.block_on(start(&opts))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
49
src/page.rs
49
src/page.rs
@ -1,9 +1,10 @@
|
||||
pub mod store;
|
||||
|
||||
#[allow(unused)]
|
||||
use anyhow::Result;
|
||||
use lazy_static::lazy_static;
|
||||
pub use store::{InMemoryStore, Store, StoreMut};
|
||||
|
||||
use anyhow::Result;
|
||||
use digest::Digest;
|
||||
|
||||
pub type Id = String;
|
||||
@ -18,6 +19,7 @@ pub struct Source(String);
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct Parsed {
|
||||
pub source: Source,
|
||||
pub source_body: String,
|
||||
pub html: String,
|
||||
pub headers: Headers,
|
||||
pub tags: Vec<Tag>,
|
||||
@ -36,7 +38,9 @@ fn split_headers_and_body(source: &Source) -> (&str, &str) {
|
||||
|
||||
if let Some(cap) = RE.captures_iter(&source.0).next() {
|
||||
(
|
||||
cap.get(1).expect("be there").as_str(),
|
||||
// important: trimming headers, prevent them from accumulating newlines in the output
|
||||
// during rewrites
|
||||
cap.get(1).expect("be there").as_str().trim(),
|
||||
cap.get(2).expect("be there").as_str(),
|
||||
)
|
||||
} else {
|
||||
@ -92,25 +96,51 @@ impl Headers {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn to_markdown_string(&self) -> String {
|
||||
"<!---\n".to_string() + &self.all + "\n-->\n"
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_tags(body: &str) -> Vec<String> {
|
||||
lazy_static! {
|
||||
static ref RE: regex::Regex = regex::Regex::new(r"#([a-zA-Z0-9]+)").expect("correct regex");
|
||||
}
|
||||
|
||||
RE.captures_iter(&body)
|
||||
.map(|m| m.get(1).expect("a value").as_str().to_lowercase())
|
||||
.collect()
|
||||
}
|
||||
|
||||
impl Parsed {
|
||||
fn from_markdown(source: Source) -> Parsed {
|
||||
fn from_full_source(source: Source) -> Parsed {
|
||||
let (headers, body) = split_headers_and_body(&source);
|
||||
let headers = Headers::parse(headers, &source);
|
||||
|
||||
let parser = pulldown_cmark::Parser::new(body);
|
||||
Self::from_headers_and_body(headers, body.to_owned())
|
||||
}
|
||||
|
||||
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 mut html_output = String::new();
|
||||
pulldown_cmark::html::push_html(&mut html_output, parser);
|
||||
|
||||
let tags = parse_tags(&body);
|
||||
|
||||
Parsed {
|
||||
headers,
|
||||
html: html_output,
|
||||
source,
|
||||
tags: vec!["TODO".into()],
|
||||
source_body: body,
|
||||
source: Source(source),
|
||||
tags,
|
||||
title: "TODO".into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_new_source_body(&self, new_body_source: &str) -> Self {
|
||||
Self::from_headers_and_body(self.headers.clone(), new_body_source.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -137,7 +167,7 @@ c: d "#
|
||||
|
||||
#[test]
|
||||
fn parse_markdown_metadata_test() -> Result<()> {
|
||||
let page = Parsed::from_markdown(Source(
|
||||
let page = Parsed::from_full_source(Source(
|
||||
r#"
|
||||
|
||||
<!---
|
||||
@ -159,8 +189,3 @@ tagwiki-id: 123
|
||||
assert_eq!(page.headers.id, "xyz");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_to_store(_store: &impl Store, source: Source) -> Result<()> {
|
||||
let _page = Parsed::from_markdown(source);
|
||||
Ok(())
|
||||
}
|
||||
|
@ -127,17 +127,10 @@ pub struct InMemoryStore {
|
||||
}
|
||||
|
||||
impl InMemoryStore {
|
||||
#[allow(unused)]
|
||||
pub fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
/*
|
||||
fn inner(&self) -> Result<std::sync::MutexGuard<InMemoryStoreInner>> {
|
||||
self.inner
|
||||
.lock()
|
||||
.map_err(|e| format_err!("Lock failed {}", e))
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
|
@ -20,7 +20,7 @@ impl FsStore {
|
||||
..Self::default()
|
||||
};
|
||||
for entry in walkdir::WalkDir::new(&s.root_path) {
|
||||
match Self::try_reading_page_from_entry_res(entry) {
|
||||
match s.try_reading_page_from_entry_res(entry) {
|
||||
Ok(Some((page, path))) => {
|
||||
s.id_to_path.insert(page.headers.id.clone(), path.clone());
|
||||
s.path_to_page.insert(path, page);
|
||||
@ -64,14 +64,16 @@ impl FsStore {
|
||||
}
|
||||
|
||||
fn try_reading_page_from_entry_res(
|
||||
&self,
|
||||
entry: walkdir::Result<walkdir::DirEntry>,
|
||||
) -> Result<Option<(page::Parsed, PathBuf)>> {
|
||||
let entry = entry?;
|
||||
Self::try_reading_page_from_entry(&entry)
|
||||
self.try_reading_page_from_entry(&entry)
|
||||
.with_context(|| format!("While reading path: {}", entry.path().display()))
|
||||
}
|
||||
|
||||
fn try_reading_page_from_entry(
|
||||
&self,
|
||||
entry: &walkdir::DirEntry,
|
||||
) -> Result<Option<(page::Parsed, PathBuf)>> {
|
||||
if !entry.file_type().is_file() {
|
||||
@ -88,13 +90,38 @@ impl FsStore {
|
||||
reader.read_to_string(&mut source.0)?;
|
||||
|
||||
Ok(Some((
|
||||
page::Parsed::from_markdown(source),
|
||||
entry.path().to_owned(),
|
||||
page::Parsed::from_full_source(source),
|
||||
entry
|
||||
.path()
|
||||
.strip_prefix(&self.root_path)
|
||||
.expect("correct prefix")
|
||||
.to_owned(),
|
||||
)))
|
||||
}
|
||||
|
||||
fn write_page_to_file(&self, _rel_path: &Path, _page: &page::Parsed) -> Result<()> {
|
||||
todo!();
|
||||
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 tmp_path = path.with_extension(format!("md.tmp.{}", crate::util::random_string(8)));
|
||||
|
||||
tokio::task::spawn_blocking(move || -> Result<()> {
|
||||
let mut file = std::fs::File::create(&tmp_path)?;
|
||||
file.write_all(b"<!---\n")?;
|
||||
file.write_all(page.headers.all.as_bytes())?;
|
||||
file.write_all(b"\n-->\n")?;
|
||||
file.write_all(page.source_body.as_bytes())?;
|
||||
|
||||
file.flush()?;
|
||||
file.sync_data()?;
|
||||
drop(file);
|
||||
|
||||
std::fs::rename(tmp_path, path)?;
|
||||
Ok(())
|
||||
})
|
||||
.await??;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@ -114,7 +141,7 @@ impl page::StoreMut for FsStore {
|
||||
self.title_to_new_rel_path(&page.title)
|
||||
};
|
||||
|
||||
self.write_page_to_file(&path, &page)?;
|
||||
self.write_page_to_file(&path, &page).await?;
|
||||
self.id_to_path
|
||||
.insert(page.headers.id.clone(), path.clone());
|
||||
self.path_to_page.insert(path, page.clone());
|
||||
|
Loading…
Reference in New Issue
Block a user