Add NewArticleResource
Serve placeholder page for non-existing articles. Redirect user-generated slugs to canonical slugs.
This commit is contained in:
parent
01dafa7d37
commit
0a3cb53a66
8 changed files with 156 additions and 54 deletions
|
@ -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,
|
||||||
|
|
|
@ -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
6
src/mimes.rs
Normal 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();
|
||||||
|
}
|
89
src/new_article_resource.rs
Normal file
89
src/new_article_resource.rs
Normal 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
32
src/rendering.rs
Normal 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
|
||||||
|
}
|
|
@ -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!")
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -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>
|
||||||
})))
|
})))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue