]>
Commit | Line | Data |
---|---|---|
2c00a5a8 XL |
1 | // Copyright 2012-2018 The Rust Project Developers. See the COPYRIGHT |
2 | // file at the top-level directory of this distribution and at | |
3 | // http://rust-lang.org/COPYRIGHT. | |
4 | // | |
5 | // Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or | |
6 | // http://www.apache.org/licenses/LICENSE-2.0> or the MIT license | |
7 | // <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your | |
8 | // option. This file may not be copied, modified, or distributed | |
9 | // except according to those terms. | |
10 | ||
11 | use std::collections::HashSet; | |
12 | use std::fs::File; | |
13 | use std::hash::{Hash, Hasher}; | |
14 | use std::io::Read; | |
15 | use std::path::Path; | |
16 | ||
17 | macro_rules! try_something { | |
18 | ($e:expr, $out:expr) => ({ | |
19 | match $e { | |
20 | Ok(c) => c, | |
21 | Err(e) => { | |
22 | eprintln!("rustdoc: got an error: {}", e); | |
23 | return $out; | |
24 | } | |
25 | } | |
26 | }) | |
27 | } | |
28 | ||
29 | #[derive(Debug, Clone, Eq)] | |
30 | pub struct CssPath { | |
31 | pub name: String, | |
32 | pub children: HashSet<CssPath>, | |
33 | } | |
34 | ||
35 | // This PartialEq implementation IS NOT COMMUTATIVE!!! | |
36 | // | |
37 | // The order is very important: the second object must have all first's rules. | |
38 | // However, the first doesn't require to have all second's rules. | |
39 | impl PartialEq for CssPath { | |
40 | fn eq(&self, other: &CssPath) -> bool { | |
41 | if self.name != other.name { | |
42 | false | |
43 | } else { | |
44 | for child in &self.children { | |
45 | if !other.children.iter().any(|c| child == c) { | |
46 | return false; | |
47 | } | |
48 | } | |
49 | true | |
50 | } | |
51 | } | |
52 | } | |
53 | ||
54 | impl Hash for CssPath { | |
55 | fn hash<H: Hasher>(&self, state: &mut H) { | |
56 | self.name.hash(state); | |
57 | for x in &self.children { | |
58 | x.hash(state); | |
59 | } | |
60 | } | |
61 | } | |
62 | ||
63 | impl CssPath { | |
64 | fn new(name: String) -> CssPath { | |
65 | CssPath { | |
66 | name, | |
67 | children: HashSet::new(), | |
68 | } | |
69 | } | |
70 | } | |
71 | ||
72 | /// All variants contain the position they occur. | |
73 | #[derive(Debug, Clone, Copy)] | |
74 | enum Events { | |
75 | StartLineComment(usize), | |
76 | StartComment(usize), | |
77 | EndComment(usize), | |
78 | InBlock(usize), | |
79 | OutBlock(usize), | |
80 | } | |
81 | ||
82 | impl Events { | |
83 | fn get_pos(&self) -> usize { | |
84 | match *self { | |
85 | Events::StartLineComment(p) | | |
86 | Events::StartComment(p) | | |
87 | Events::EndComment(p) | | |
88 | Events::InBlock(p) | | |
89 | Events::OutBlock(p) => p, | |
90 | } | |
91 | } | |
92 | ||
93 | fn is_comment(&self) -> bool { | |
94 | match *self { | |
95 | Events::StartLineComment(_) | | |
96 | Events::StartComment(_) | | |
97 | Events::EndComment(_) => true, | |
98 | _ => false, | |
99 | } | |
100 | } | |
101 | } | |
102 | ||
103 | fn previous_is_line_comment(events: &[Events]) -> bool { | |
104 | if let Some(&Events::StartLineComment(_)) = events.last() { | |
105 | true | |
106 | } else { | |
107 | false | |
108 | } | |
109 | } | |
110 | ||
111 | fn is_line_comment(pos: usize, v: &[u8], events: &[Events]) -> bool { | |
112 | if let Some(&Events::StartComment(_)) = events.last() { | |
113 | return false; | |
114 | } | |
115 | pos + 1 < v.len() && v[pos + 1] == b'/' | |
116 | } | |
117 | ||
118 | fn load_css_events(v: &[u8]) -> Vec<Events> { | |
119 | let mut pos = 0; | |
120 | let mut events = Vec::with_capacity(100); | |
121 | ||
122 | while pos < v.len() - 1 { | |
123 | match v[pos] { | |
124 | b'/' if pos + 1 < v.len() && v[pos + 1] == b'*' => { | |
125 | events.push(Events::StartComment(pos)); | |
126 | pos += 1; | |
127 | } | |
128 | b'/' if is_line_comment(pos, v, &events) => { | |
129 | events.push(Events::StartLineComment(pos)); | |
130 | pos += 1; | |
131 | } | |
132 | b'\n' if previous_is_line_comment(&events) => { | |
133 | events.push(Events::EndComment(pos)); | |
134 | } | |
135 | b'*' if pos + 1 < v.len() && v[pos + 1] == b'/' => { | |
136 | events.push(Events::EndComment(pos + 2)); | |
137 | pos += 1; | |
138 | } | |
139 | b'{' if !previous_is_line_comment(&events) => { | |
140 | if let Some(&Events::StartComment(_)) = events.last() { | |
141 | pos += 1; | |
142 | continue | |
143 | } | |
144 | events.push(Events::InBlock(pos + 1)); | |
145 | } | |
146 | b'}' if !previous_is_line_comment(&events) => { | |
147 | if let Some(&Events::StartComment(_)) = events.last() { | |
148 | pos += 1; | |
149 | continue | |
150 | } | |
151 | events.push(Events::OutBlock(pos + 1)); | |
152 | } | |
153 | _ => {} | |
154 | } | |
155 | pos += 1; | |
156 | } | |
157 | events | |
158 | } | |
159 | ||
160 | fn get_useful_next(events: &[Events], pos: &mut usize) -> Option<Events> { | |
161 | while *pos < events.len() { | |
162 | if !events[*pos].is_comment() { | |
163 | return Some(events[*pos]); | |
164 | } | |
165 | *pos += 1; | |
166 | } | |
167 | None | |
168 | } | |
169 | ||
170 | fn get_previous_positions(events: &[Events], mut pos: usize) -> Vec<usize> { | |
171 | let mut ret = Vec::with_capacity(3); | |
172 | ||
173 | ret.push(events[pos].get_pos()); | |
174 | if pos > 0 { | |
175 | pos -= 1; | |
176 | } | |
177 | loop { | |
178 | if pos < 1 || !events[pos].is_comment() { | |
179 | let x = events[pos].get_pos(); | |
180 | if *ret.last().unwrap() != x { | |
181 | ret.push(x); | |
182 | } else { | |
183 | ret.push(0); | |
184 | } | |
185 | break | |
186 | } | |
187 | ret.push(events[pos].get_pos()); | |
188 | pos -= 1; | |
189 | } | |
190 | if ret.len() & 1 != 0 && events[pos].is_comment() { | |
191 | ret.push(0); | |
192 | } | |
193 | ret.iter().rev().cloned().collect() | |
194 | } | |
195 | ||
196 | fn build_rule(v: &[u8], positions: &[usize]) -> String { | |
197 | positions.chunks(2) | |
198 | .map(|x| ::std::str::from_utf8(&v[x[0]..x[1]]).unwrap_or("")) | |
199 | .collect::<String>() | |
200 | .trim() | |
201 | .replace("\n", " ") | |
202 | .replace("/", "") | |
203 | .replace("\t", " ") | |
204 | .replace("{", "") | |
205 | .replace("}", "") | |
206 | .split(" ") | |
207 | .filter(|s| s.len() > 0) | |
208 | .collect::<Vec<&str>>() | |
209 | .join(" ") | |
210 | } | |
211 | ||
212 | fn inner(v: &[u8], events: &[Events], pos: &mut usize) -> HashSet<CssPath> { | |
213 | let mut paths = Vec::with_capacity(50); | |
214 | ||
215 | while *pos < events.len() { | |
216 | if let Some(Events::OutBlock(_)) = get_useful_next(events, pos) { | |
217 | *pos += 1; | |
218 | break | |
219 | } | |
220 | if let Some(Events::InBlock(_)) = get_useful_next(events, pos) { | |
221 | paths.push(CssPath::new(build_rule(v, &get_previous_positions(events, *pos)))); | |
222 | *pos += 1; | |
223 | } | |
224 | while let Some(Events::InBlock(_)) = get_useful_next(events, pos) { | |
225 | if let Some(ref mut path) = paths.last_mut() { | |
226 | for entry in inner(v, events, pos).iter() { | |
227 | path.children.insert(entry.clone()); | |
228 | } | |
229 | } | |
230 | } | |
231 | if let Some(Events::OutBlock(_)) = get_useful_next(events, pos) { | |
232 | *pos += 1; | |
233 | } | |
234 | } | |
235 | paths.iter().cloned().collect() | |
236 | } | |
237 | ||
238 | pub fn load_css_paths(v: &[u8]) -> CssPath { | |
239 | let events = load_css_events(v); | |
240 | let mut pos = 0; | |
241 | ||
242 | let mut parent = CssPath::new("parent".to_owned()); | |
243 | parent.children = inner(v, &events, &mut pos); | |
244 | parent | |
245 | } | |
246 | ||
247 | pub fn get_differences(against: &CssPath, other: &CssPath, v: &mut Vec<String>) { | |
248 | if against.name != other.name { | |
249 | return | |
250 | } else { | |
251 | for child in &against.children { | |
252 | let mut found = false; | |
253 | let mut found_working = false; | |
254 | let mut tmp = Vec::new(); | |
255 | ||
256 | for other_child in &other.children { | |
257 | if child.name == other_child.name { | |
258 | if child != other_child { | |
259 | get_differences(child, other_child, &mut tmp); | |
260 | } else { | |
261 | found_working = true; | |
262 | } | |
263 | found = true; | |
264 | break | |
265 | } | |
266 | } | |
267 | if found == false { | |
268 | v.push(format!(" Missing \"{}\" rule", child.name)); | |
269 | } else if found_working == false { | |
270 | v.extend(tmp.iter().cloned()); | |
271 | } | |
272 | } | |
273 | } | |
274 | } | |
275 | ||
276 | pub fn test_theme_against<P: AsRef<Path>>(f: &P, against: &CssPath) -> (bool, Vec<String>) { | |
277 | let mut file = try_something!(File::open(f), (false, Vec::new())); | |
278 | let mut data = Vec::with_capacity(1000); | |
279 | ||
280 | try_something!(file.read_to_end(&mut data), (false, Vec::new())); | |
281 | let paths = load_css_paths(&data); | |
282 | let mut ret = Vec::new(); | |
283 | get_differences(against, &paths, &mut ret); | |
284 | (true, ret) | |
285 | } | |
286 | ||
287 | #[cfg(test)] | |
288 | mod test { | |
289 | use super::*; | |
290 | ||
291 | #[test] | |
292 | fn test_comments_in_rules() { | |
293 | let text = r#" | |
294 | rule a {} | |
295 | ||
296 | rule b, c | |
297 | // a line comment | |
298 | {} | |
299 | ||
300 | rule d | |
301 | // another line comment | |
302 | e {} | |
303 | ||
304 | rule f/* a multine | |
305 | ||
306 | comment*/{} | |
307 | ||
308 | rule g/* another multine | |
309 | ||
310 | comment*/h | |
311 | ||
312 | i {} | |
313 | ||
314 | rule j/*commeeeeent | |
315 | ||
316 | you like things like "{}" in there? :) | |
317 | */ | |
318 | end {}"#; | |
319 | ||
320 | let against = r#" | |
321 | rule a {} | |
322 | ||
323 | rule b, c {} | |
324 | ||
325 | rule d e {} | |
326 | ||
327 | rule f {} | |
328 | ||
329 | rule gh i {} | |
330 | ||
331 | rule j end {} | |
332 | "#; | |
333 | ||
334 | let mut ret = Vec::new(); | |
335 | get_differences(&load_css_paths(against.as_bytes()), | |
336 | &load_css_paths(text.as_bytes()), | |
337 | &mut ret); | |
338 | assert!(ret.is_empty()); | |
339 | } | |
340 | ||
341 | #[test] | |
342 | fn test_text() { | |
343 | let text = r#" | |
344 | a | |
345 | /* sdfs | |
346 | */ b | |
347 | c // sdf | |
348 | d {} | |
349 | "#; | |
350 | let paths = load_css_paths(text.as_bytes()); | |
351 | assert!(paths.children.contains(&CssPath::new("a b c d".to_owned()))); | |
352 | } | |
353 | ||
354 | #[test] | |
355 | fn test_comparison() { | |
356 | let x = r#" | |
357 | a { | |
358 | b { | |
359 | c {} | |
360 | } | |
361 | } | |
362 | "#; | |
363 | ||
364 | let y = r#" | |
365 | a { | |
366 | b {} | |
367 | } | |
368 | "#; | |
369 | ||
370 | let against = load_css_paths(y.as_bytes()); | |
371 | let other = load_css_paths(x.as_bytes()); | |
372 | ||
373 | let mut ret = Vec::new(); | |
374 | get_differences(&against, &other, &mut ret); | |
375 | assert!(ret.is_empty()); | |
376 | get_differences(&other, &against, &mut ret); | |
377 | assert_eq!(ret, vec![" Missing \"c\" rule".to_owned()]); | |
378 | } | |
379 | } |