| 203 |
203 |
|
/// has nowhere useful to return an error to.
|
| 204 |
204 |
|
pub fn check_breakpoints(frontend: impl AsRef<Path>, tuning_widths: &[u16]) {
|
| 205 |
205 |
|
let frontend = frontend.as_ref();
|
|
206 |
+ |
let mut files = files_with_extension(&frontend.join("css"), "css");
|
|
207 |
+ |
files.extend(js_files(&frontend.join("js")));
|
|
208 |
+ |
check_paths(&files, tuning_widths, Some(frontend));
|
|
209 |
+ |
}
|
|
210 |
+ |
|
|
211 |
+ |
/// [`check_breakpoints`] against a named list of files rather than a tree.
|
|
212 |
+ |
///
|
|
213 |
+ |
/// For a frontend whose generated and hand-written files share a directory, so
|
|
214 |
+ |
/// there is nothing to point a directory scan at: the MNW server keeps both
|
|
215 |
+ |
/// under `static/` alongside a bundler's output, and bundled third-party CSS
|
|
216 |
+ |
/// is exactly the place a width nobody chose would come from.
|
|
217 |
+ |
///
|
|
218 |
+ |
/// The cost is that the list is hand-maintained, and a stylesheet nobody adds
|
|
219 |
+ |
/// to it is unchecked rather than failing. Prefer [`check_breakpoints`] where
|
|
220 |
+ |
/// the layout allows it.
|
|
221 |
+ |
///
|
|
222 |
+ |
/// A `.js` path is parsed as script and anything else as stylesheet, which is
|
|
223 |
+ |
/// the only difference: a media condition is parenthesised in both.
|
|
224 |
+ |
///
|
|
225 |
+ |
/// # Panics
|
|
226 |
+ |
///
|
|
227 |
+ |
/// If a listed file cannot be read -- a listed path that no longer exists is a
|
|
228 |
+ |
/// check silently covering less than it says -- or if any width is neither a
|
|
229 |
+ |
/// size-class boundary nor a declared tuning width.
|
|
230 |
+ |
pub fn check_breakpoints_files<P: AsRef<Path>>(paths: &[P], tuning_widths: &[u16]) {
|
|
231 |
+ |
let paths: Vec<PathBuf> = paths.iter().map(|p| p.as_ref().to_path_buf()).collect();
|
|
232 |
+ |
check_paths(&paths, tuning_widths, None);
|
|
233 |
+ |
}
|
|
234 |
+ |
|
|
235 |
+ |
/// The check itself. `root`, when given, is stripped from reported paths.
|
|
236 |
+ |
fn check_paths(paths: &[PathBuf], tuning_widths: &[u16], root: Option<&Path>) {
|
| 206 |
237 |
|
let allowed = allowed_widths(tuning_widths);
|
| 207 |
238 |
|
let mut stale: Vec<String> = Vec::new();
|
| 208 |
239 |
|
|
| 209 |
|
- |
let css_files = files_with_extension(&frontend.join("css"), "css");
|
| 210 |
|
- |
for path in &css_files {
|
| 211 |
|
- |
let raw = std::fs::read_to_string(path).expect("read css file");
|
|
240 |
+ |
for path in paths {
|
|
241 |
+ |
let raw = std::fs::read_to_string(path)
|
|
242 |
+ |
.unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
|
|
243 |
+ |
let name = match root {
|
|
244 |
+ |
Some(root) => display_name(root, path),
|
|
245 |
+ |
None => path.display().to_string(),
|
|
246 |
+ |
};
|
|
247 |
+ |
|
|
248 |
+ |
if path.extension().is_some_and(|x| x == "js") {
|
|
249 |
+ |
// No declarations in JS, so any parenthesised width is a query.
|
|
250 |
+ |
for (offset, px) in js_widths(&raw) {
|
|
251 |
+ |
if !allowed.contains(&px) {
|
|
252 |
+ |
stale.push(format!(" {name}:{} ({px}px)", line_of(&raw, offset)));
|
|
253 |
+ |
}
|
|
254 |
+ |
}
|
|
255 |
+ |
continue;
|
|
256 |
+ |
}
|
|
257 |
+ |
|
| 212 |
258 |
|
// Comments first: a note about a breakpoint that used to be here is
|
| 213 |
259 |
|
// prose, not a rule, and should not fail a build.
|
| 214 |
260 |
|
let src = strip_block_comments(&raw);
|
| 215 |
|
- |
let name = display_name(frontend, path);
|
| 216 |
261 |
|
for (offset, condition) in media_conditions(&src) {
|
| 217 |
262 |
|
for px in media_widths(condition) {
|
| 218 |
263 |
|
if !allowed.contains(&px) {
|
| 225 |
270 |
|
}
|
| 226 |
271 |
|
}
|
| 227 |
272 |
|
|
| 228 |
|
- |
let js_files = js_files(&frontend.join("js"));
|
| 229 |
|
- |
for path in &js_files {
|
| 230 |
|
- |
let src = std::fs::read_to_string(path).expect("read js file");
|
| 231 |
|
- |
let name = display_name(frontend, path);
|
| 232 |
|
- |
// No declarations in JS, so any width condition is a media query.
|
| 233 |
|
- |
for (offset, px) in js_widths(&src) {
|
| 234 |
|
- |
if !allowed.contains(&px) {
|
| 235 |
|
- |
stale.push(format!(" {name}:{} ({px}px)", line_of(&src, offset)));
|
| 236 |
|
- |
}
|
| 237 |
|
- |
}
|
| 238 |
|
- |
}
|
| 239 |
|
- |
|
| 240 |
273 |
|
assert!(
|
| 241 |
274 |
|
stale.is_empty(),
|
| 242 |
275 |
|
"hand-written breakpoints disagree with makeover_geometry::SizeClass.\n\n\
|
| 245 |
278 |
|
Stale:\n{}\n\n\
|
| 246 |
279 |
|
If a size class moved, update these to match. If one of these is a new\n\
|
| 247 |
280 |
|
tuning width inside the wide shell rather than a shell boundary, add it\n\
|
| 248 |
|
- |
to the caller's tuning list with a note saying what it tunes.",
|
|
281 |
+ |
to the caller's tuning list with a note saying what it tunes.\n\n\
|
|
282 |
+ |
Best of all, make the rule dimensional so it needs no threshold: a grid\n\
|
|
283 |
+ |
wants repeat(auto-fit, minmax(<content floor>, 1fr)) and a size wants\n\
|
|
284 |
+ |
clamp(). A threshold is for what appears and disappears.",
|
| 249 |
285 |
|
allowed
|
| 250 |
286 |
|
.iter()
|
| 251 |
287 |
|
.filter(|px| !tuning_widths.contains(px))
|
| 253 |
289 |
|
stale.join("\n")
|
| 254 |
290 |
|
);
|
| 255 |
291 |
|
|
| 256 |
|
- |
for path in css_files.iter().chain(&js_files) {
|
|
292 |
+ |
for path in paths {
|
| 257 |
293 |
|
println!("cargo:rerun-if-changed={}", path.display());
|
| 258 |
294 |
|
}
|
| 259 |
295 |
|
}
|
| 544 |
580 |
|
assert!(found.is_err(), "a nested stylesheet must be scanned");
|
| 545 |
581 |
|
}
|
| 546 |
582 |
|
|
|
583 |
+ |
#[test]
|
|
584 |
+ |
fn a_named_list_is_checked() {
|
|
585 |
+ |
let dir = frontend("bp-list");
|
|
586 |
+ |
write(&dir, "css/style.css", "@media (max-width: 768px) { }\n");
|
|
587 |
+ |
let listed = dir.join("css/style.css");
|
|
588 |
+ |
let err =
|
|
589 |
+ |
std::panic::catch_unwind(|| check_breakpoints_files(&[&listed], &[])).unwrap_err();
|
|
590 |
+ |
let msg = err.downcast_ref::<String>().expect("String payload");
|
|
591 |
+ |
assert!(msg.contains("style.css:1"), "got: {msg}");
|
|
592 |
+ |
}
|
|
593 |
+ |
|
|
594 |
+ |
#[test]
|
|
595 |
+ |
#[should_panic(expected = "read ")]
|
|
596 |
+ |
fn a_listed_file_that_is_gone_fails() {
|
|
597 |
+ |
// The list is hand-maintained, so a path that stopped existing is a
|
|
598 |
+ |
// check quietly covering less than it claims. Louder than skipping it.
|
|
599 |
+ |
let dir = frontend("bp-missing");
|
|
600 |
+ |
check_breakpoints_files(&[dir.join("css/never-written.css")], &[]);
|
|
601 |
+ |
}
|
|
602 |
+ |
|
|
603 |
+ |
#[test]
|
|
604 |
+ |
fn a_listed_js_file_is_parsed_as_script() {
|
|
605 |
+ |
// The unparenthesized-declaration rule is what separates the two, and
|
|
606 |
+ |
// picking the parser off the extension is the whole difference.
|
|
607 |
+ |
let dir = frontend("bp-list-js");
|
|
608 |
+ |
write(
|
|
609 |
+ |
&dir,
|
|
610 |
+ |
"js/style.js",
|
|
611 |
+ |
"el.style.cssText = 'max-width: 320px';\n",
|
|
612 |
+ |
);
|
|
613 |
+ |
check_breakpoints_files(&[dir.join("js/style.js")], &[]);
|
|
614 |
+ |
}
|
|
615 |
+ |
|
| 547 |
616 |
|
#[test]
|
| 548 |
617 |
|
fn the_error_names_the_file_and_line() {
|
| 549 |
618 |
|
let dir = frontend("bp-message");
|