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 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<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 {
|
||||
state: State,
|
||||
article_id: i32,
|
||||
|
|
|
@ -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;
|
||||
|
|
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
|
||||
.filter(article_revisions::article_id.eq(article_id))
|
||||
.filter(article_revisions::revision.eq(new_revision))
|
||||
.load::<models::ArticleRevision>(&*conn)?
|
||||
.pop()
|
||||
.expect("We just inserted this row!")
|
||||
.first::<models::ArticleRevision>(&*conn)?
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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<Resource + Sync + Send>
|
||||
)));
|
||||
}
|
||||
|
||||
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<Resource + Sync + Send>,
|
||||
SlugLookup::Hit { article_id, revision } =>
|
||||
Some(Box::new(ArticleResource::new(state, article_id, revision))
|
||||
as Box<Resource + Sync + Send>),
|
||||
Box::new(ArticleResource::new(state, article_id, revision))
|
||||
as Box<Resource + Sync + Send>,
|
||||
SlugLookup::Redirect(slug) =>
|
||||
Some(Box::new(ArticleRedirectResource::new(slug))
|
||||
as Box<Resource + Sync + Send>)
|
||||
Box::new(ArticleRedirectResource::new(slug))
|
||||
as Box<Resource + Sync + Send>
|
||||
})))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,12 +9,12 @@
|
|||
<form action="" method="POST">
|
||||
|
||||
<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>
|
||||
|
||||
<article>
|
||||
<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>
|
||||
</article>
|
||||
|
||||
|
|
Loading…
Reference in a new issue