370 lines
8.8 KiB
Rust
370 lines
8.8 KiB
Rust
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()]);
|
|
}
|
|
}
|