From 0a3cb53a66f1269875a9c9c16c36ee03a395ec27 Mon Sep 17 00:00:00 2001 From: Magnus Hoff Date: Thu, 21 Sep 2017 10:58:54 +0200 Subject: [PATCH] Add NewArticleResource Serve placeholder page for non-existing articles. Redirect user-generated slugs to canonical slugs. --- src/article_resource.rs | 43 +--------------- src/main.rs | 3 ++ src/mimes.rs | 6 +++ src/new_article_resource.rs | 89 +++++++++++++++++++++++++++++++++ src/rendering.rs | 32 ++++++++++++ src/state.rs | 4 +- src/wiki_lookup.rs | 29 ++++++++--- templates/article_revision.html | 4 +- 8 files changed, 156 insertions(+), 54 deletions(-) create mode 100644 src/mimes.rs create mode 100644 src/new_article_resource.rs create mode 100644 src/rendering.rs diff --git a/src/article_resource.rs b/src/article_resource.rs index d065e54..a492d09 100644 --- a/src/article_resource.rs +++ b/src/article_resource.rs @@ -1,56 +1,17 @@ use futures::{self, Future}; use hyper; use hyper::header::ContentType; -use hyper::mime; use hyper::server::*; use serde_json; use serde_urlencoded; use assets::{StyleCss, ScriptJs}; +use mimes::*; +use rendering::render_markdown; use site::Layout; use state::State; use web::{Resource, ResponseFuture}; -lazy_static! { - static ref TEXT_HTML: mime::Mime = "text/html;charset=utf-8".parse().unwrap(); - static ref APPLICATION_JSON: mime::Mime = "application/json".parse().unwrap(); -} - -fn render_markdown(src: &str) -> String { - use pulldown_cmark::Event; - - struct EscapeHtml<'a, I: Iterator>> { - inner: I, - } - - impl<'a, I: Iterator>> EscapeHtml<'a, I> { - fn new(inner: I) -> EscapeHtml<'a, I> { - EscapeHtml { inner } - } - } - - impl<'a, I: Iterator>> Iterator for EscapeHtml<'a, I> { - type Item = Event<'a>; - - fn next(&mut self) -> Option { - use pulldown_cmark::Event::{Text, Html, InlineHtml}; - - match self.inner.next() { - Some(Html(x)) => Some(Text(x)), - Some(InlineHtml(x)) => Some(Text(x)), - x => x - } - } - } - - use pulldown_cmark::{Parser, html}; - - let p = EscapeHtml::new(Parser::new(src)); - let mut buf = String::new(); - html::push_html(&mut buf, p); - buf -} - pub struct ArticleResource { state: State, article_id: i32, diff --git a/src/main.rs b/src/main.rs index 3f207cb..5a543c3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,7 +25,10 @@ mod article_redirect_resource; mod article_resource; mod assets; mod db; +mod mimes; mod models; +mod new_article_resource; +mod rendering; mod schema; mod site; mod state; diff --git a/src/mimes.rs b/src/mimes.rs new file mode 100644 index 0000000..6af57e5 --- /dev/null +++ b/src/mimes.rs @@ -0,0 +1,6 @@ +use hyper::mime; + +lazy_static! { + pub static ref TEXT_HTML: mime::Mime = "text/html;charset=utf-8".parse().unwrap(); + pub static ref APPLICATION_JSON: mime::Mime = "application/json".parse().unwrap(); +} diff --git a/src/new_article_resource.rs b/src/new_article_resource.rs new file mode 100644 index 0000000..da667ea --- /dev/null +++ b/src/new_article_resource.rs @@ -0,0 +1,89 @@ +use futures::{self, Future}; +use hyper; +use hyper::header::ContentType; +use hyper::server::*; + +use assets::{StyleCss, ScriptJs}; +use mimes::*; +use site::Layout; +use state::State; +use web::{Resource, ResponseFuture}; + +const NDASH: &str = "\u{2013}"; + +const EMPTY_ARTICLE_MESSAGE: &str = " +

Not found

+

There's no article here yet. You can create one by clicking the +edit-link below and saving a new article.

+"; + +fn title_from_slug(slug: &str) -> String { + slug.replace('-', " ") +} + +pub struct NewArticleResource { + state: State, + slug: String, +} + +impl NewArticleResource { + pub fn new(state: State, slug: String) -> Self { + Self { state, slug } + } +} + +impl Resource for NewArticleResource { + fn allow(&self) -> Vec { + use hyper::Method::*; + vec![Options, Head, Get, Put] + } + + fn head(&self) -> ResponseFuture { + Box::new(futures::finished(Response::new() + .with_status(hyper::StatusCode::NotFound) + .with_header(ContentType(TEXT_HTML.clone())) + )) + } + + fn get(self: Box) -> ResponseFuture { + #[derive(BartDisplay)] + #[template="templates/article_revision.html"] + struct Template<'a> { + article_id: &'a str, + revision: &'a str, + created: &'a str, + + slug: &'a str, + title: &'a str, + raw: &'a str, + rendered: &'a str, + + script_js_checksum: &'a str, + } + + let title = title_from_slug(&self.slug); + + Box::new(self.head() + .and_then(move |head| { + Ok(head + .with_body(Layout { + title: &title, + body: &Template { + article_id: NDASH, + revision: NDASH, + created: NDASH, + slug: &self.slug, + title: &title, + raw: "", + rendered: EMPTY_ARTICLE_MESSAGE, + script_js_checksum: ScriptJs::checksum(), + }, + style_css_checksum: StyleCss::checksum(), + }.to_string())) + })) + } + + fn put(self: Box, _body: hyper::Body) -> ResponseFuture { + unimplemented!() + } +} diff --git a/src/rendering.rs b/src/rendering.rs new file mode 100644 index 0000000..a7311be --- /dev/null +++ b/src/rendering.rs @@ -0,0 +1,32 @@ +use pulldown_cmark::{Event, Parser, html}; + +struct EscapeHtml<'a, I: Iterator>> { + inner: I, +} + +impl<'a, I: Iterator>> EscapeHtml<'a, I> { + fn new(inner: I) -> EscapeHtml<'a, I> { + EscapeHtml { inner } + } +} + +impl<'a, I: Iterator>> Iterator for EscapeHtml<'a, I> { + type Item = Event<'a>; + + fn next(&mut self) -> Option { + use pulldown_cmark::Event::{Text, Html, InlineHtml}; + + match self.inner.next() { + Some(Html(x)) => Some(Text(x)), + Some(InlineHtml(x)) => Some(Text(x)), + x => x + } + } +} + +pub fn render_markdown(src: &str) -> String { + let p = EscapeHtml::new(Parser::new(src)); + let mut buf = String::new(); + html::push_html(&mut buf, p); + buf +} diff --git a/src/state.rs b/src/state.rs index ddcfaef..81be9cc 100644 --- a/src/state.rs +++ b/src/state.rs @@ -193,9 +193,7 @@ impl State { Ok(article_revisions::table .filter(article_revisions::article_id.eq(article_id)) .filter(article_revisions::revision.eq(new_revision)) - .load::(&*conn)? - .pop() - .expect("We just inserted this row!") + .first::(&*conn)? ) }) }) diff --git a/src/wiki_lookup.rs b/src/wiki_lookup.rs index 7eb9f76..0428fa4 100644 --- a/src/wiki_lookup.rs +++ b/src/wiki_lookup.rs @@ -5,6 +5,7 @@ use futures::{Future, finished}; use assets::*; use article_resource::ArticleResource; use article_redirect_resource::ArticleRedirectResource; +use new_article_resource::NewArticleResource; use state::State; use web::{Lookup, Resource}; @@ -63,25 +64,37 @@ impl Lookup for WikiLookup { let mut split = path[1..].split('/'); - let slug = split.next().expect("Always at least one element"); + let slug = split.next().expect("Always at least one element").to_owned(); if split.next() != None { // Currently disallow any URLs of the form /slug/... return Box::new(finished(None)); } + // Normalize all user-generated slugs: + let slugified_slug = ::slug::slugify(&slug); + if slugified_slug != slug { + return Box::new(finished(Some( + Box::new(ArticleRedirectResource::new(slugified_slug)) + as Box + ))); + } + let state = self.state.clone(); use state::SlugLookup; - Box::new(self.state.lookup_slug(slug.to_owned()) - .and_then(|x| Ok(match x { - SlugLookup::Miss => None, + Box::new(self.state.lookup_slug(slug.clone()) + .and_then(|x| Ok(Some(match x { + SlugLookup::Miss => + Box::new(NewArticleResource::new(state, slug)) + as Box, SlugLookup::Hit { article_id, revision } => - Some(Box::new(ArticleResource::new(state, article_id, revision)) - as Box), + Box::new(ArticleResource::new(state, article_id, revision)) + as Box, SlugLookup::Redirect(slug) => - Some(Box::new(ArticleRedirectResource::new(slug)) - as Box) + Box::new(ArticleRedirectResource::new(slug)) + as Box }))) + ) } } diff --git a/templates/article_revision.html b/templates/article_revision.html index 0658dab..b920330 100644 --- a/templates/article_revision.html +++ b/templates/article_revision.html @@ -9,12 +9,12 @@
-

+

- +