| use rustc_data_structures::fx::FxHashSet; |
| use std::fs; |
| use std::hash::{Hash, Hasher}; |
| use std::path::Path; |
| |
| use errors::Handler; |
| |
| macro_rules! try_something { |
| ($e:expr, $diag:expr, $out:expr) => ({ |
| match $e { |
| Ok(c) => c, |
| Err(e) => { |
| $diag.struct_err(&e.to_string()).emit(); |
| return $out; |
| } |
| } |
| }) |
| } |
| |
| #[derive(Debug, Clone, Eq)] |
| pub struct CssPath { |
| pub name: String, |
| pub children: FxHashSet<CssPath>, |
| } |
| |
| // This PartialEq implementation IS NOT COMMUTATIVE!!! |
| // |
| // The order is very important: the second object must have all first's rules. |
| // However, the first doesn't require to have all second's rules. |
| impl PartialEq for CssPath { |
| fn eq(&self, other: &CssPath) -> bool { |
| if self.name != other.name { |
| false |
| } else { |
| for child in &self.children { |
| if !other.children.iter().any(|c| child == c) { |
| return false; |
| } |
| } |
| true |
| } |
| } |
| } |
| |
| impl Hash for CssPath { |
| fn hash<H: Hasher>(&self, state: &mut H) { |
| self.name.hash(state); |
| for x in &self.children { |
| x.hash(state); |
| } |
| } |
| } |
| |
| impl CssPath { |
| fn new(name: String) -> CssPath { |
| CssPath { |
| name, |
| children: FxHashSet::default(), |
| } |
| } |
| } |
| |
| /// All variants contain the position they occur. |
| #[derive(Debug, Clone, Copy)] |
| enum Events { |
| StartLineComment(usize), |
| StartComment(usize), |
| EndComment(usize), |
| InBlock(usize), |
| OutBlock(usize), |
| } |
| |
| impl Events { |
| fn get_pos(&self) -> usize { |
| match *self { |
| Events::StartLineComment(p) | |
| Events::StartComment(p) | |
| Events::EndComment(p) | |
| Events::InBlock(p) | |
| Events::OutBlock(p) => p, |
| } |
| } |
| |
| fn is_comment(&self) -> bool { |
| match *self { |
| Events::StartLineComment(_) | |
| Events::StartComment(_) | |
| Events::EndComment(_) => true, |
| _ => false, |
| } |
| } |
| } |
| |
| fn previous_is_line_comment(events: &[Events]) -> bool { |
| if let Some(&Events::StartLineComment(_)) = events.last() { |
| true |
| } else { |
| false |
| } |
| } |
| |
| fn is_line_comment(pos: usize, v: &[u8], events: &[Events]) -> bool { |
| if let Some(&Events::StartComment(_)) = events.last() { |
| return false; |
| } |
| pos + 1 < v.len() && v[pos + 1] == b'/' |
| } |
| |
| fn load_css_events(v: &[u8]) -> Vec<Events> { |
| let mut pos = 0; |
| let mut events = Vec::with_capacity(100); |
| |
| while pos < v.len() - 1 { |
| match v[pos] { |
| b'/' if pos + 1 < v.len() && v[pos + 1] == b'*' => { |
| events.push(Events::StartComment(pos)); |
| pos += 1; |
| } |
| b'/' if is_line_comment(pos, v, &events) => { |
| events.push(Events::StartLineComment(pos)); |
| pos += 1; |
| } |
| b'\n' if previous_is_line_comment(&events) => { |
| events.push(Events::EndComment(pos)); |
| } |
| b'*' if pos + 1 < v.len() && v[pos + 1] == b'/' => { |
| events.push(Events::EndComment(pos + 2)); |
| pos += 1; |
| } |
| b'{' if !previous_is_line_comment(&events) => { |
| if let Some(&Events::StartComment(_)) = events.last() { |
| pos += 1; |
| continue |
| } |
| events.push(Events::InBlock(pos + 1)); |
| } |
| b'}' if !previous_is_line_comment(&events) => { |
| if let Some(&Events::StartComment(_)) = events.last() { |
| pos += 1; |
| continue |
| } |
| events.push(Events::OutBlock(pos + 1)); |
| } |
| _ => {} |
| } |
| pos += 1; |
| } |
| events |
| } |
| |
| fn get_useful_next(events: &[Events], pos: &mut usize) -> Option<Events> { |
| while *pos < events.len() { |
| if !events[*pos].is_comment() { |
| return Some(events[*pos]); |
| } |
| *pos += 1; |
| } |
| None |
| } |
| |
| fn get_previous_positions(events: &[Events], mut pos: usize) -> Vec<usize> { |
| let mut ret = Vec::with_capacity(3); |
| |
| ret.push(events[pos].get_pos()); |
| if pos > 0 { |
| pos -= 1; |
| } |
| loop { |
| if pos < 1 || !events[pos].is_comment() { |
| let x = events[pos].get_pos(); |
| if *ret.last().unwrap() != x { |
| ret.push(x); |
| } else { |
| ret.push(0); |
| } |
| break |
| } |
| ret.push(events[pos].get_pos()); |
| pos -= 1; |
| } |
| if ret.len() & 1 != 0 && events[pos].is_comment() { |
| ret.push(0); |
| } |
| ret.iter().rev().cloned().collect() |
| } |
| |
| fn build_rule(v: &[u8], positions: &[usize]) -> String { |
| positions.chunks(2) |
| .map(|x| ::std::str::from_utf8(&v[x[0]..x[1]]).unwrap_or("")) |
| .collect::<String>() |
| .trim() |
| .replace("\n", " ") |
| .replace("/", "") |
| .replace("\t", " ") |
| .replace("{", "") |
| .replace("}", "") |
| .split(' ') |
| .filter(|s| s.len() > 0) |
| .collect::<Vec<&str>>() |
| .join(" ") |
| } |
| |
| fn inner(v: &[u8], events: &[Events], pos: &mut usize) -> FxHashSet<CssPath> { |
| let mut paths = Vec::with_capacity(50); |
| |
| while *pos < events.len() { |
| if let Some(Events::OutBlock(_)) = get_useful_next(events, pos) { |
| *pos += 1; |
| break |
| } |
| if let Some(Events::InBlock(_)) = get_useful_next(events, pos) { |
| paths.push(CssPath::new(build_rule(v, &get_previous_positions(events, *pos)))); |
| *pos += 1; |
| } |
| while let Some(Events::InBlock(_)) = get_useful_next(events, pos) { |
| if let Some(ref mut path) = paths.last_mut() { |
| for entry in inner(v, events, pos).iter() { |
| path.children.insert(entry.clone()); |
| } |
| } |
| } |
| if let Some(Events::OutBlock(_)) = get_useful_next(events, pos) { |
| *pos += 1; |
| } |
| } |
| paths.iter().cloned().collect() |
| } |
| |
| pub fn load_css_paths(v: &[u8]) -> CssPath { |
| let events = load_css_events(v); |
| let mut pos = 0; |
| |
| let mut parent = CssPath::new("parent".to_owned()); |
| parent.children = inner(v, &events, &mut pos); |
| parent |
| } |
| |
| pub fn get_differences(against: &CssPath, other: &CssPath, v: &mut Vec<String>) { |
| if against.name != other.name { |
| return |
| } else { |
| for child in &against.children { |
| let mut found = false; |
| let mut found_working = false; |
| let mut tmp = Vec::new(); |
| |
| for other_child in &other.children { |
| if child.name == other_child.name { |
| if child != other_child { |
| get_differences(child, other_child, &mut tmp); |
| } else { |
| found_working = true; |
| } |
| found = true; |
| break |
| } |
| } |
| if found == false { |
| v.push(format!(" Missing \"{}\" rule", child.name)); |
| } else if found_working == false { |
| v.extend(tmp.iter().cloned()); |
| } |
| } |
| } |
| } |
| |
| pub fn test_theme_against<P: AsRef<Path>>(f: &P, against: &CssPath, diag: &Handler) |
| -> (bool, Vec<String>) |
| { |
| let data = try_something!(fs::read(f), diag, (false, vec![])); |
| let paths = load_css_paths(&data); |
| let mut ret = vec![]; |
| get_differences(against, &paths, &mut ret); |
| (true, ret) |
| } |
| |
| #[cfg(test)] |
| mod test { |
| use super::*; |
| |
| #[test] |
| fn test_comments_in_rules() { |
| let text = r#" |
| rule a {} |
| |
| rule b, c |
| // a line comment |
| {} |
| |
| rule d |
| // another line comment |
| e {} |
| |
| rule f/* a multine |
| |
| comment*/{} |
| |
| rule g/* another multine |
| |
| comment*/h |
| |
| i {} |
| |
| rule j/*commeeeeent |
| |
| you like things like "{}" in there? :) |
| */ |
| end {}"#; |
| |
| let against = r#" |
| rule a {} |
| |
| rule b, c {} |
| |
| rule d e {} |
| |
| rule f {} |
| |
| rule gh i {} |
| |
| rule j end {} |
| "#; |
| |
| let mut ret = Vec::new(); |
| get_differences(&load_css_paths(against.as_bytes()), |
| &load_css_paths(text.as_bytes()), |
| &mut ret); |
| assert!(ret.is_empty()); |
| } |
| |
| #[test] |
| fn test_text() { |
| let text = r#" |
| a |
| /* sdfs |
| */ b |
| c // sdf |
| d {} |
| "#; |
| let paths = load_css_paths(text.as_bytes()); |
| assert!(paths.children.contains(&CssPath::new("a b c d".to_owned()))); |
| } |
| |
| #[test] |
| fn test_comparison() { |
| let x = r#" |
| a { |
| b { |
| c {} |
| } |
| } |
| "#; |
| |
| let y = r#" |
| a { |
| b {} |
| } |
| "#; |
| |
| let against = load_css_paths(y.as_bytes()); |
| let other = load_css_paths(x.as_bytes()); |
| |
| let mut ret = Vec::new(); |
| get_differences(&against, &other, &mut ret); |
| assert!(ret.is_empty()); |
| get_differences(&other, &against, &mut ret); |
| assert_eq!(ret, vec![" Missing \"c\" rule".to_owned()]); |
| } |
| } |