Add NewArticleResource

Serve placeholder page for non-existing articles.
Redirect user-generated slugs to canonical slugs.
This commit is contained in:
Magnus Hoff 2017-09-21 10:58:54 +02:00
parent 01dafa7d37
commit 0a3cb53a66
8 changed files with 156 additions and 54 deletions

View file

@ -1,56 +1,17 @@
use futures::{self, Future}; use futures::{self, Future};
use hyper; use hyper;
use hyper::header::ContentType; use hyper::header::ContentType;
use hyper::mime;
use hyper::server::*; use hyper::server::*;
use serde_json; use serde_json;
use serde_urlencoded; use serde_urlencoded;
use assets::{StyleCss, ScriptJs}; use assets::{StyleCss, ScriptJs};
use mimes::*;
use rendering::render_markdown;
use site::Layout; use site::Layout;
use state::State; use state::State;
use web::{Resource, ResponseFuture}; 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<Item=Event<'a>>> {
inner: I,
}
impl<'a, I: Iterator<Item=Event<'a>>> EscapeHtml<'a, I> {
fn new(inner: I) -> EscapeHtml<'a, I> {
EscapeHtml { inner }
}
}
impl<'a, I: Iterator<Item=Event<'a>>> Iterator for EscapeHtml<'a, I> {
type Item = Event<'a>;
fn next(&mut self) -> Option<Self::Item> {
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 { pub struct ArticleResource {
state: State, state: State,
article_id: i32, article_id: i32,

View file

@ -25,7 +25,10 @@ mod article_redirect_resource;
mod article_resource; mod article_resource;
mod assets; mod assets;
mod db; mod db;
mod mimes;
mod models; mod models;
mod new_article_resource;
mod rendering;
mod schema; mod schema;
mod site; mod site;
mod state; mod state;

6
src/mimes.rs Normal file
View file

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

View file

@ -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 = "
<p>Not found</p>
<p>There's no article here yet. You can create one by clicking the
edit-link below and saving a new article.</p>
";
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<hyper::Method> {
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<Self>) -> 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<Self>, _body: hyper::Body) -> ResponseFuture {
unimplemented!()
}
}

32
src/rendering.rs Normal file
View file

@ -0,0 +1,32 @@
use pulldown_cmark::{Event, Parser, html};
struct EscapeHtml<'a, I: Iterator<Item=Event<'a>>> {
inner: I,
}
impl<'a, I: Iterator<Item=Event<'a>>> EscapeHtml<'a, I> {
fn new(inner: I) -> EscapeHtml<'a, I> {
EscapeHtml { inner }
}
}
impl<'a, I: Iterator<Item=Event<'a>>> Iterator for EscapeHtml<'a, I> {
type Item = Event<'a>;
fn next(&mut self) -> Option<Self::Item> {
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
}

View file

@ -193,9 +193,7 @@ impl State {
Ok(article_revisions::table Ok(article_revisions::table
.filter(article_revisions::article_id.eq(article_id)) .filter(article_revisions::article_id.eq(article_id))
.filter(article_revisions::revision.eq(new_revision)) .filter(article_revisions::revision.eq(new_revision))
.load::<models::ArticleRevision>(&*conn)? .first::<models::ArticleRevision>(&*conn)?
.pop()
.expect("We just inserted this row!")
) )
}) })
}) })

View file

@ -5,6 +5,7 @@ use futures::{Future, finished};
use assets::*; use assets::*;
use article_resource::ArticleResource; use article_resource::ArticleResource;
use article_redirect_resource::ArticleRedirectResource; use article_redirect_resource::ArticleRedirectResource;
use new_article_resource::NewArticleResource;
use state::State; use state::State;
use web::{Lookup, Resource}; use web::{Lookup, Resource};
@ -63,25 +64,37 @@ impl Lookup for WikiLookup {
let mut split = path[1..].split('/'); 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 { if split.next() != None {
// Currently disallow any URLs of the form /slug/... // Currently disallow any URLs of the form /slug/...
return Box::new(finished(None)); 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<Resource + Sync + Send>
)));
}
let state = self.state.clone(); let state = self.state.clone();
use state::SlugLookup; use state::SlugLookup;
Box::new(self.state.lookup_slug(slug.to_owned()) Box::new(self.state.lookup_slug(slug.clone())
.and_then(|x| Ok(match x { .and_then(|x| Ok(Some(match x {
SlugLookup::Miss => None, SlugLookup::Miss =>
Box::new(NewArticleResource::new(state, slug))
as Box<Resource + Sync + Send>,
SlugLookup::Hit { article_id, revision } => SlugLookup::Hit { article_id, revision } =>
Some(Box::new(ArticleResource::new(state, article_id, revision)) Box::new(ArticleResource::new(state, article_id, revision))
as Box<Resource + Sync + Send>), as Box<Resource + Sync + Send>,
SlugLookup::Redirect(slug) => SlugLookup::Redirect(slug) =>
Some(Box::new(ArticleRedirectResource::new(slug)) Box::new(ArticleRedirectResource::new(slug))
as Box<Resource + Sync + Send>) as Box<Resource + Sync + Send>
}))) })))
)
} }
} }

View file

@ -9,12 +9,12 @@
<form action="" method="POST"> <form action="" method="POST">
<header> <header>
<h1><input autocomplete=off type=text name=title value="{{title}}"></h1> <h1><input autocomplete=off type=text name=title value="{{title}}" placeholder="Title"></h1>
</header> </header>
<article> <article>
<input autocomplete=off type=hidden name=base_revision value="{{revision}}"> <input autocomplete=off type=hidden name=base_revision value="{{revision}}">
<textarea autocomplete=off name=body>{{raw}}</textarea> <textarea autocomplete=off name=body placeholder="Article goes here">{{raw}}</textarea>
<textarea autocomplete=off class="shadow-control"></textarea> <textarea autocomplete=off class="shadow-control"></textarea>
</article> </article>