diff --git a/Cargo.lock b/Cargo.lock index 8ff3b38..ac91a05 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -178,6 +178,11 @@ dependencies = [ "diesel 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "diff" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "digest" version = "0.6.2" @@ -574,6 +579,7 @@ dependencies = [ "clap 2.26.2 (registry+https://github.com/rust-lang/crates.io-index)", "diesel 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)", "diesel_codegen 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)", + "diff 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", "futures-cpupool 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", "hyper 0.11.2 (registry+https://github.com/rust-lang/crates.io-index)", @@ -959,6 +965,7 @@ dependencies = [ "checksum diesel 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)" = "304226fa7a3982b0405f6bb95dd9c10c3e2000709f194038a60ec2c277150951" "checksum diesel_codegen 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)" = "18a42ca5c9b660add51d58bc5a50a87123380e1e458069c5504528a851ed7384" "checksum diesel_infer_schema 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bf1957ff5cd3b04772e43c162c2f69c2aa918080ff9b020276792d236be8be52" +"checksum diff 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "0a515461b6c8c08419850ced27bc29e86166dcdcde8fbe76f8b1f0589bb49472" "checksum digest 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e5b29bf156f3f4b3c4f610a25ff69370616ae6e0657d416de22645483e72af0a" "checksum dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "09c3753c3db574d215cba4ea76018483895d7bff25a31b49ba45db21c48e50ab" "checksum either 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "18785c1ba806c258137c937e44ada9ee7e69a37e3c72077542cd2f069d78562a" diff --git a/Cargo.toml b/Cargo.toml index db45fcd..b3cfcf3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ bart = "0.1.4" bart_derive = "0.1.4" chrono = "0.4" clap = "2.26" +diff = "0.1.10" futures = "0.1" futures-cpupool = "0.1" hyper = "0.11" diff --git a/assets/style.css b/assets/style.css index a72667f..2662c0a 100644 --- a/assets/style.css +++ b/assets/style.css @@ -405,3 +405,13 @@ input[type="search"] { max-height: 500px; } } + +.removed { + background: red; +} +.same { + background: white; +} +.added { + background: green; +} diff --git a/src/main.rs b/src/main.rs index eeed675..1ca09bc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,7 @@ extern crate chrono; extern crate clap; +extern crate diff; extern crate futures; extern crate futures_cpupool; extern crate percent_encoding; diff --git a/src/resources/diff_resource.rs b/src/resources/diff_resource.rs new file mode 100644 index 0000000..2ff60cf --- /dev/null +++ b/src/resources/diff_resource.rs @@ -0,0 +1,225 @@ +use std; +use std::str::FromStr; + +use diff; +use futures::{self, Future}; +use futures::future::{done, finished}; +use hyper; +use hyper::header::ContentType; +use hyper::server::*; +use serde_urlencoded; + +use mimes::*; +use models::ArticleRevision; +use site::Layout; +use state::State; +use web::{Resource, ResponseFuture}; + +const NONE: &str = "none"; + +type BoxResource = Box; +type Error = Box; + +#[derive(Clone)] +pub enum ArticleRevisionReference { + None, + Some { + article_id: u32, + revision: u32, + } +} + +use std::num::ParseIntError; + +pub enum ArticleRevisionReferenceParseError { + SplitError, + ParseIntError(ParseIntError), +} + +use std::fmt; +impl fmt::Display for ArticleRevisionReferenceParseError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use self::ArticleRevisionReferenceParseError::*; + match self { + &SplitError => write!(f, "invalid format, must contain one @"), + &ParseIntError(ref r) => r.fmt(f) + } + } +} + +impl From for ArticleRevisionReferenceParseError { + fn from(x: ParseIntError) -> ArticleRevisionReferenceParseError { + ArticleRevisionReferenceParseError::ParseIntError(x) + } +} + +impl FromStr for ArticleRevisionReference { + type Err = ArticleRevisionReferenceParseError; + + fn from_str(s: &str) -> Result { + if s == NONE { + return Ok(ArticleRevisionReference::None); + } + + let items: Vec<&str> = s.split("@").collect(); + if items.len() != 2 { + return Err(ArticleRevisionReferenceParseError::SplitError) + } + + let article_id = items[0].parse::()?; + let revision = items[1].parse::()?; + + Ok(ArticleRevisionReference::Some { article_id, revision }) + } +} + +impl fmt::Display for ArticleRevisionReference { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + &ArticleRevisionReference::None => write!(f, "{}", NONE), + &ArticleRevisionReference::Some { article_id, revision } => { + write!(f, "{}@{}", article_id, revision) + } + } + } +} + +use serde::{de, Deserialize, Deserializer}; +impl<'de> Deserialize<'de> for ArticleRevisionReference { + fn deserialize(deserializer: D) -> Result + where D: Deserializer<'de> + { + let s = String::deserialize(deserializer)?; + FromStr::from_str(&s).map_err(de::Error::custom) + } +} + +use serde::{Serialize, Serializer}; +impl Serialize for ArticleRevisionReference { + fn serialize(&self, serializer: S) -> Result + where S: Serializer + { + serializer.serialize_str(&self.to_string()) + } +} + +#[derive(Clone)] +pub struct DiffLookup { + state: State, +} + +#[derive(Serialize, Deserialize)] +pub struct QueryParameters { + from: ArticleRevisionReference, + to: ArticleRevisionReference, +} + +impl QueryParameters { + pub fn new(from: ArticleRevisionReference, to: ArticleRevisionReference) -> QueryParameters { + QueryParameters { from, to } + } + + pub fn into_link(self) -> String { + serde_urlencoded::to_string(self).expect("Serializing to String cannot fail") + } +} + +impl DiffLookup { + pub fn new(state: State) -> DiffLookup { + Self { state } + } + + pub fn lookup(&self, query: Option<&str>) -> Box, Error=::web::Error>> { + let state = self.state.clone(); + + Box::new(done((|| -> Result, ::web::Error> { + let params: QueryParameters = serde_urlencoded::from_str(query.unwrap_or(""))?; + + Ok(Some(Box::new(DiffResource::new(state, params.from, params.to)))) + }()))) + } +} + +pub struct DiffResource { + state: State, + from: ArticleRevisionReference, + to: ArticleRevisionReference, +} + +impl DiffResource { + pub fn new(state: State, from: ArticleRevisionReference, to: ArticleRevisionReference) -> Self { + Self { state, from, to } + } + + fn query_args(&self) -> QueryParameters { + QueryParameters { + from: self.from.clone(), + to: self.to.clone(), + } + } + + fn get_article_revision(&self, r: &ArticleRevisionReference) -> Box, Error = Error>> { + match r { + &ArticleRevisionReference::None => Box::new(finished(None)), + &ArticleRevisionReference::Some { article_id, revision } => Box::new( + self.state.get_article_revision(article_id as i32, revision as i32) + ), + } + } +} + +impl Resource for DiffResource { + fn allow(&self) -> Vec { + use hyper::Method::*; + vec![Options, Head, Get] + } + + fn head(&self) -> ResponseFuture { + Box::new(futures::finished(Response::new() + .with_status(hyper::StatusCode::Ok) + .with_header(ContentType(TEXT_HTML.clone())) + )) + } + + fn get(self: Box) -> ResponseFuture { + #[derive(BartDisplay)] + #[template = "templates/diff.html"] + struct Template<'a> { + lines: &'a [DiffLine<'a>], + } + + #[derive(Default)] + struct DiffLine<'a> { + removed: Option<&'a str>, + same: Option<&'a str>, + added: Option<&'a str>, + } + + let from = self.get_article_revision(&self.from); + let to = self.get_article_revision(&self.to); + + let head = self.head(); + + Box::new(head.join3(from, to) + .and_then(move |(head, from, to)| { + Ok(head + .with_body(Layout { + base: None, // Hmm, should perhaps accept `base` as argument + title: "Difference", + body: &Template { + lines: &diff::lines( + from.as_ref().map(|x| &*x.body).unwrap_or(""), + to.as_ref().map(|x| &*x.body).unwrap_or("") + ) + .into_iter() + .map(|x| match x { + diff::Result::Left(x) => DiffLine { removed: Some(x), ..Default::default() }, + diff::Result::Both(x, _) => DiffLine { same: Some(x), ..Default::default() }, + diff::Result::Right(x) => DiffLine { added: Some(x), ..Default::default() }, + }) + .collect::>() + }, + }.to_string())) + })) + } +} diff --git a/src/resources/mod.rs b/src/resources/mod.rs index 0791e0e..748b4c1 100644 --- a/src/resources/mod.rs +++ b/src/resources/mod.rs @@ -3,6 +3,7 @@ pub mod pagination; mod article_revision_resource; mod article_resource; mod changes_resource; +mod diff_resource; mod new_article_resource; mod search_resource; mod sitemap_resource; @@ -11,6 +12,7 @@ mod temporary_redirect_resource; pub use self::article_revision_resource::ArticleRevisionResource; pub use self::article_resource::ArticleResource; pub use self::changes_resource::{ChangesLookup, ChangesResource}; +pub use self::diff_resource::{DiffLookup, DiffResource}; pub use self::new_article_resource::NewArticleResource; pub use self::search_resource::SearchLookup; pub use self::sitemap_resource::SitemapResource; diff --git a/src/wiki_lookup.rs b/src/wiki_lookup.rs index 4c9736f..63a61fd 100644 --- a/src/wiki_lookup.rs +++ b/src/wiki_lookup.rs @@ -47,6 +47,7 @@ lazy_static! { pub struct WikiLookup { state: State, changes_lookup: ChangesLookup, + diff_lookup: DiffLookup, search_lookup: SearchLookup, } @@ -78,9 +79,10 @@ fn asset_lookup(path: &str) -> FutureResult, Box<::std::erro impl WikiLookup { pub fn new(state: State, show_authors: bool) -> WikiLookup { let changes_lookup = ChangesLookup::new(state.clone(), show_authors); + let diff_lookup = DiffLookup::new(state.clone()); let search_lookup = SearchLookup::new(state.clone()); - WikiLookup { state, changes_lookup, search_lookup } + WikiLookup { state, changes_lookup, diff_lookup, search_lookup } } fn revisions_lookup(&self, path: &str, _query: Option<&str>) -> ::Future { @@ -143,6 +145,8 @@ impl WikiLookup { self.by_id_lookup(tail, query), ("_changes", None) => Box::new(self.changes_lookup.lookup(query)), + ("_diff", None) => + Box::new(self.diff_lookup.lookup(query)), ("_new", None) => Box::new(finished(Some(Box::new(NewArticleResource::new(self.state.clone(), None)) as BoxResource))), ("_revisions", Some(tail)) => diff --git a/templates/diff.html b/templates/diff.html new file mode 100644 index 0000000..45fac61 --- /dev/null +++ b/templates/diff.html @@ -0,0 +1,14 @@ +
+
+

Diff

+
+ +
+
{{#lines}}{{#.removed}}
{{.}} +
{{/.removed}}{{#.same}}
{{.}} +
{{/.same}}{{#.added}}
{{.}} +
{{/.added}}{{/lines}}
+
+
+ +{{>footer/default.html}}