| 1 |
1 |
|
use std::fmt::Write as _;
|
| 2 |
2 |
|
use std::fs;
|
| 3 |
|
- |
use std::path::{Path, PathBuf};
|
|
3 |
+ |
use std::path::Path;
|
| 4 |
4 |
|
|
| 5 |
5 |
|
use makeover_geometry::SizeClass;
|
| 6 |
6 |
|
use makeover_layout::{Column, Priority, Width};
|
| 469 |
469 |
|
const REVIEWED_ELEMENT_OVERLAPS: &[(&str, &str, &str)] = &[];
|
| 470 |
470 |
|
|
| 471 |
471 |
|
/// Every file that can carry a class name.
|
| 472 |
|
- |
fn markup_files(frontend: &Path) -> Vec<PathBuf> {
|
| 473 |
|
- |
// `index.html` and `compose.html` were the first two entries until the
|
| 474 |
|
- |
// 2026-08-22 swap deleted them. What is left under `js/` is the generated
|
| 475 |
|
- |
// scripts, and they are still read: `quasi-selection.js` names the classes
|
| 476 |
|
- |
// quasi-webview emits, so a class it reaches for that the stylesheet does
|
| 477 |
|
- |
// not define is the same defect this check was written to catch.
|
| 478 |
|
- |
let mut files = Vec::new();
|
| 479 |
|
- |
let js = fs::read_dir(frontend.join("js")).expect("read frontend/js");
|
| 480 |
|
- |
let mut scripts: Vec<PathBuf> = js
|
| 481 |
|
- |
.map(|entry| entry.expect("read frontend/js entry").path())
|
| 482 |
|
- |
.filter(|path| path.extension().is_some_and(|ext| ext == "js"))
|
| 483 |
|
- |
.collect();
|
| 484 |
|
- |
// Sorted, or the order is the filesystem's and two machines seal against
|
| 485 |
|
- |
// the same set in a different order. It makes no difference to the count
|
| 486 |
|
- |
// and every difference to reading a diff of the warning.
|
| 487 |
|
- |
scripts.sort();
|
| 488 |
|
- |
files.extend(scripts);
|
| 489 |
|
- |
files
|
|
472 |
+ |
/// Which of this app's stylesheet rules match something the app can emit.
|
|
473 |
+ |
///
|
|
474 |
+ |
/// goingson `43a682b0`, and the restoration of `check_vocabulary_use`.
|
|
475 |
+ |
///
|
|
476 |
+ |
/// # What was lost, and why it could not be patched
|
|
477 |
+ |
///
|
|
478 |
+ |
/// That check asked "which generated classes does no markup emit" and answered
|
|
479 |
+ |
/// it by reading `index.html`, `compose.html` and every file under
|
|
480 |
+ |
/// `frontend/js/`. The 2026-08-22 swap deleted all of them. The markup is
|
|
481 |
+ |
/// `quasi-webview`'s emitter now, in another crate, so this repo cannot see its
|
|
482 |
+ |
/// own document.
|
|
483 |
+ |
///
|
|
484 |
+ |
/// Raising `DEAD_VOCABULARY_HIGH_WATER` from 42 to 73 would have made the build
|
|
485 |
+ |
/// green and would have reported clean about a 9,830-line stylesheet nobody is
|
|
486 |
+ |
/// reading. So the check was left out until the question could be asked
|
|
487 |
+ |
/// properly, which is what this is.
|
|
488 |
+ |
///
|
|
489 |
+ |
/// # Asked of the other side, which is what makes it exact
|
|
490 |
+ |
///
|
|
491 |
+ |
/// The old check pointed at the markup and asked what it used. This points at
|
|
492 |
+ |
/// the stylesheet and asks what it styles, because the set of emittable classes
|
|
493 |
+ |
/// is now knowable: `quasi_webview::vocabulary::covers` is every class a
|
|
494 |
+ |
/// document served by this app can contain, and it is **closed**. A description
|
|
495 |
+ |
/// has no word for a class -- `src/quasi/` names zero of them -- so nothing but
|
|
496 |
+ |
/// the two renderer crates can put one in the document.
|
|
497 |
+ |
///
|
|
498 |
+ |
/// That closure is the whole value. A selector outside the set is dead, not
|
|
499 |
+ |
/// merely unaccounted for, and a build that says "1,731 dead" is saying
|
|
500 |
+ |
/// something a person can act on rather than something they have to go and
|
|
501 |
+ |
/// check.
|
|
502 |
+ |
///
|
|
503 |
+ |
/// # Sealed, not fixed
|
|
504 |
+ |
///
|
|
505 |
+ |
/// The count is large and shrinking it is `daac5cc7`'s job, not this one's. So
|
|
506 |
+ |
/// this seals at a high water and refuses only growth, the same shape every
|
|
507 |
+ |
/// other drift check in this file has. The number in `DEAD_SELECTOR_HIGH_WATER`
|
|
508 |
+ |
/// is a measurement; when the stylesheet is cut, the build says so and the seal
|
|
509 |
+ |
/// comes down with it.
|
|
510 |
+ |
fn check_stylesheet_reaches_markup(frontend: &Path) {
|
|
511 |
+ |
let opts = Emit::default();
|
|
512 |
+ |
|
|
513 |
+ |
let mut dead: Vec<String> = Vec::new();
|
|
514 |
+ |
let mut styled = 0usize;
|
|
515 |
+ |
for sheet in APP_STYLESHEETS {
|
|
516 |
+ |
let path = frontend.join("css").join(sheet);
|
|
517 |
+ |
println!("cargo::rerun-if-changed={}", path.display());
|
|
518 |
+ |
let css =
|
|
519 |
+ |
fs::read_to_string(&path).unwrap_or_else(|error| panic!("read css/{sheet}: {error}"));
|
|
520 |
+ |
for class in class_selectors(&css) {
|
|
521 |
+ |
if quasi_webview::vocabulary::covers(&class, &opts) {
|
|
522 |
+ |
styled += 1;
|
|
523 |
+ |
} else {
|
|
524 |
+ |
dead.push(format!("{sheet}: .{class}"));
|
|
525 |
+ |
}
|
|
526 |
+ |
}
|
|
527 |
+ |
}
|
|
528 |
+ |
dead.sort();
|
|
529 |
+ |
dead.dedup();
|
|
530 |
+ |
|
|
531 |
+ |
assert!(
|
|
532 |
+ |
dead.len() <= DEAD_SELECTOR_HIGH_WATER,
|
|
533 |
+ |
"{} selectors in this app's stylesheets match no class anything can \
|
|
534 |
+ |
emit, above the recorded {}. Either a rule was written for markup that \
|
|
535 |
+ |
does not exist, or quasi-webview stopped emitting something:\n{}",
|
|
536 |
+ |
dead.len(),
|
|
537 |
+ |
DEAD_SELECTOR_HIGH_WATER,
|
|
538 |
+ |
dead.iter()
|
|
539 |
+ |
.map(|one| format!(" {one}"))
|
|
540 |
+ |
.collect::<Vec<_>>()
|
|
541 |
+ |
.join("\n")
|
|
542 |
+ |
);
|
|
543 |
+ |
|
|
544 |
+ |
if dead.len() < DEAD_SELECTOR_HIGH_WATER {
|
|
545 |
+ |
println!(
|
|
546 |
+ |
"cargo::warning=dead stylesheet selectors are down to {} from a \
|
|
547 |
+ |
sealed {}; lower the seal in build.rs so they cannot grow back",
|
|
548 |
+ |
dead.len(),
|
|
549 |
+ |
DEAD_SELECTOR_HIGH_WATER
|
|
550 |
+ |
);
|
|
551 |
+ |
}
|
|
552 |
+ |
|
|
553 |
+ |
// Said whether or not anything is wrong. The two numbers together are the
|
|
554 |
+ |
// measurement `43a682b0` asked for, and a count that only appears on
|
|
555 |
+ |
// failure is a count nobody watches move.
|
|
556 |
+ |
println!(
|
|
557 |
+ |
"cargo::warning=stylesheet reaches markup: {styled} live selectors, {} dead",
|
|
558 |
+ |
dead.len()
|
|
559 |
+ |
);
|
| 490 |
560 |
|
}
|
| 491 |
561 |
|
|
|
562 |
+ |
/// The stylesheets this app hand-writes or generates, in cascade order.
|
|
563 |
+ |
///
|
|
564 |
+ |
/// `typography.css` and `geometry.css` are generated by makeover-build and
|
|
565 |
+ |
/// carry custom properties rather than class rules, so they contribute almost
|
|
566 |
+ |
/// nothing here; they are read anyway because a generated sheet that grew a
|
|
567 |
+ |
/// class rule is exactly the drift worth catching.
|
|
568 |
+ |
const APP_STYLESHEETS: &[&str] = &[
|
|
569 |
+ |
"typography.css",
|
|
570 |
+ |
"geometry.css",
|
|
571 |
+ |
"layout.css",
|
|
572 |
+ |
"tables.css",
|
|
573 |
+ |
"styles.css",
|
|
574 |
+ |
];
|
|
575 |
+ |
|
|
576 |
+ |
/// How many dead selectors this app is known to carry.
|
|
577 |
+ |
///
|
|
578 |
+ |
/// A measurement, taken 2026-08-22 by this function. Not a target: `daac5cc7`
|
|
579 |
+ |
/// is the task that cuts them, and this number comes down with it.
|
|
580 |
+ |
const DEAD_SELECTOR_HIGH_WATER: usize = 1021;
|
|
581 |
+ |
|
|
582 |
+ |
/// Every class named in a selector in this CSS.
|
|
583 |
+ |
///
|
|
584 |
+ |
/// Deliberately crude, and correct for what it is asked. It finds `.name` in
|
|
585 |
+ |
/// selector position and stops a name at the first character CSS does not allow
|
|
586 |
+ |
/// in one. It does not parse.
|
|
587 |
+ |
///
|
|
588 |
+ |
/// # Selector position is not "depth zero"
|
|
589 |
+ |
///
|
|
590 |
+ |
/// The first version of this tracked brace depth and read classes only at zero,
|
|
591 |
+ |
/// which is wrong in the direction that matters: `@media`, `@supports` and
|
|
592 |
+ |
/// `@layer` open a block whose contents are more rules, so every responsive
|
|
593 |
+ |
/// rule in the sheet sat at depth one and was skipped. It reported 96 dead
|
|
594 |
+ |
/// selectors against a 9,830-line stylesheet, which is a believable-looking
|
|
595 |
+ |
/// number and a wrong one.
|
|
596 |
+ |
///
|
|
597 |
+ |
/// So a block is classified when it opens: an at-rule block holds rules, and
|
|
598 |
+ |
/// anything else holds declarations. Classes count everywhere except inside a
|
|
599 |
+ |
/// declaration block.
|
|
600 |
+ |
fn class_selectors(css: &str) -> Vec<String> {
|
|
601 |
+ |
/// What the innermost open brace was opened by.
|
|
602 |
+ |
enum Block {
|
|
603 |
+ |
/// `@media`, `@supports`, `@layer` -- more rules inside.
|
|
604 |
+ |
Rules,
|
|
605 |
+ |
/// A selector -- declarations inside, and no classes to find.
|
|
606 |
+ |
Declarations,
|
|
607 |
+ |
}
|
|
608 |
+ |
|
|
609 |
+ |
let mut found = Vec::new();
|
|
610 |
+ |
let mut stack: Vec<Block> = Vec::new();
|
|
611 |
+ |
let bytes = css.as_bytes();
|
|
612 |
+ |
// Where the current prelude began, so an opening brace can say what kind of
|
|
613 |
+ |
// block it is: at-rule preludes start with `@`.
|
|
614 |
+ |
let mut prelude = 0usize;
|
|
615 |
+ |
let mut i = 0;
|
|
616 |
+ |
while i < bytes.len() {
|
|
617 |
+ |
match bytes[i] {
|
|
618 |
+ |
b'{' => {
|
|
619 |
+ |
let starts_at_rule = css[prelude..i].trim_start().starts_with('@');
|
|
620 |
+ |
stack.push(if starts_at_rule {
|
|
621 |
+ |
Block::Rules
|
|
622 |
+ |
} else {
|
|
623 |
+ |
Block::Declarations
|
|
624 |
+ |
});
|
|
625 |
+ |
prelude = i + 1;
|
|
626 |
+ |
}
|
|
627 |
+ |
b'}' => {
|
|
628 |
+ |
stack.pop();
|
|
629 |
+ |
prelude = i + 1;
|
|
630 |
+ |
}
|
|
631 |
+ |
b';' => prelude = i + 1,
|
|
632 |
+ |
b'.' if !matches!(stack.last(), Some(Block::Declarations)) => {
|
|
633 |
+ |
let start = i + 1;
|
|
634 |
+ |
let mut end = start;
|
|
635 |
+ |
while end < bytes.len()
|
|
636 |
+ |
&& (bytes[end].is_ascii_alphanumeric()
|
|
637 |
+ |
|| bytes[end] == b'-'
|
|
638 |
+ |
|| bytes[end] == b'_')
|
|
639 |
+ |
{
|
|
640 |
+ |
end += 1;
|
|
641 |
+ |
}
|
|
642 |
+ |
// `.5rem` in selector position is a number, not a class.
|
|
643 |
+ |
if end > start && !bytes[start].is_ascii_digit() {
|
|
644 |
+ |
found.push(css[start..end].to_owned());
|
|
645 |
+ |
}
|
|
646 |
+ |
i = end;
|
|
647 |
+ |
continue;
|
|
648 |
+ |
}
|
|
649 |
+ |
_ => {}
|
|
650 |
+ |
}
|
|
651 |
+ |
i += 1;
|
|
652 |
+ |
}
|
|
653 |
+ |
found
|
|
654 |
+ |
}
|
|
655 |
+ |
|
|
656 |
+ |
// `markup_files` stood here until 2026-08-22. It gathered the files this app's
|
|
657 |
+ |
// markup lived in, and after the swap the only thing left for it to gather was
|
|
658 |
+ |
// the generated scripts quasi-webview writes into `frontend/js/`. Those are
|
|
659 |
+ |
// that crate's, and `check_stylesheet_reaches_markup` reads that crate's
|
|
660 |
+ |
// vocabulary directly, so the indirection had nothing left in it.
|
|
661 |
+ |
|
| 492 |
662 |
|
/// Widths that are tuning inside the wide shell, not a shell boundary.
|
| 493 |
663 |
|
///
|
| 494 |
664 |
|
/// A shell boundary is a [`SizeClass`] edge and belongs to makeover-geometry.
|
| 600 |
770 |
|
REVIEWED_OVERLAPS,
|
| 601 |
771 |
|
REVIEWED_ELEMENT_OVERLAPS,
|
| 602 |
772 |
|
);
|
| 603 |
|
- |
// `check_vocabulary_use` is not called here any more, and this is a hole
|
| 604 |
|
- |
// rather than a tidy-up. It asks which generated classes no markup emits,
|
| 605 |
|
- |
// and it answered that by reading `index.html`, `compose.html` and every
|
| 606 |
|
- |
// file under `frontend/js/`. The 2026-08-22 swap deleted all of them: the
|
| 607 |
|
- |
// markup is `quasi-webview`'s emitter now, which is another crate, so this
|
| 608 |
|
- |
// repo cannot see its own document to check it.
|
| 609 |
|
- |
//
|
| 610 |
|
- |
// Left uncalled rather than called with the high water raised to 73. The
|
| 611 |
|
- |
// second is what silencing it would look like, and it would report clean
|
| 612 |
|
- |
// about a stylesheet nobody is reading. Restoring the coverage means asking
|
| 613 |
|
- |
// quasi-webview which classes it emits, which is goingson `43a682b0`.
|
| 614 |
|
- |
let _ = markup_files(&frontend);
|
|
773 |
+ |
// The question `check_vocabulary_use` used to answer, asked of the thing
|
|
774 |
+ |
// that now answers it. See `check_stylesheet_reaches_markup`, and goingson
|
|
775 |
+ |
// `43a682b0` for the hole this closes.
|
|
776 |
+ |
check_stylesheet_reaches_markup(&frontend);
|
| 615 |
777 |
|
// `check_touch_density` is dropped, on the instruction the check itself
|
| 616 |
778 |
|
// gives for this case: it keeps every copy of `TOUCH_DENSITY` equal to
|
| 617 |
779 |
|
// `makeover_geometry::Density::Touch`, and after the swap this frontend
|