Fix: tighten stripe-child text and static cascade gates (#394)

Keep flex/align out of the frozen expandStaticDeclaration map, require empty tagged stripe cues on the text path, and honor inset order plus rem width.

Prepared with AI assistance.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Abdul Wahab
2026-09-10 09:58:28 +05:00
co-authored by Cursor
parent 5c67bf725d
commit fc5dcefce2
6 changed files with 172 additions and 49 deletions
+62 -26
View File
@@ -146,36 +146,64 @@ where
}
}
/// Opening-tag span that contains `index`, if any. `end` is the `>` byte.
fn markup_tag_span(line: &str, index: usize) -> Option<(usize, usize)> {
let mut i = 0usize;
while i < line.len() {
let Some(rel) = line[i..].find('<') else {
return None;
};
let tag_start = i + rel;
let after = &line[tag_start + 1..];
if !after.chars().next().is_some_and(|c| c.is_ascii_alphabetic()) {
i = tag_start + 1;
continue;
}
let mut tag_end: Option<usize> = None;
scan_js(line, tag_start + 1, |ch, j, _p, _n, depth| {
if ch == '>' && depth.brace == 0 {
tag_end = Some(j);
return true;
}
false
});
let Some(end) = tag_end else {
return None;
};
if index >= tag_start && index <= end {
return Some((tag_start, end));
}
i = end + 1;
}
None
}
/// JS: detect-text.mjs#containingMarkupTag (only its `text` is read).
fn containing_markup_tag(line: &str) -> impl Fn(usize) -> String + '_ {
move |index: usize| {
let mut i = 0usize;
while i < line.len() {
let Some(rel) = line[i..].find('<') else { break };
let tag_start = i + rel;
let after = &line[tag_start + 1..];
if !after.chars().next().is_some_and(|c| c.is_ascii_alphabetic()) {
i = tag_start + 1;
continue;
}
let mut tag_end: Option<usize> = None;
scan_js(line, tag_start + 1, |ch, j, _p, _n, depth| {
if ch == '>' && depth.brace == 0 {
tag_end = Some(j);
return true;
}
false
});
let Some(end) = tag_end else { break };
if index >= tag_start && index <= end {
return line[tag_start..end + 1].to_string();
}
i = end + 1;
}
line.to_string()
markup_tag_span(line, index)
.map(|(start, end)| line[start..end + 1].to_string())
.unwrap_or_else(|| line.to_string())
}
}
fn is_self_closing_tag(tag: &str) -> bool {
tag.trim_end_matches('>').trim_end().ends_with('/')
}
/// Text path cannot see the DOM. Require an empty or self-closing tag so a
/// `w-1 bg-amber-500` wrapper with content is not reported as a stripe.
fn stripe_child_markup_empty(line: &str, index: usize) -> bool {
let Some((start, end)) = markup_tag_span(line, index) else {
return false;
};
if is_self_closing_tag(&line[start..end + 1]) {
return true;
}
let rest = line.get(end + 1..).unwrap_or("").trim_start();
rest.is_empty() || rest.starts_with("</")
}
struct TernarySplit {
common: String,
consequent: String,
@@ -504,6 +532,10 @@ re!(
r"(?i)aria-(?:current|selected)"
);
re!(STRIPE_CHILD_ROUNDED_FULL_RE, format!("{B}rounded-full{B}"));
re!(
STRIPE_CHILD_CUE_RE,
format!("{B}(?:shrink-0|rounded-[lres](?:-{W}+)?|left-0|right-0|inset-y-0){B}")
);
/// Hyphen-safe class-token boundary: the byte before `index` must not be `-`
/// or an ASCII word character (mirrors JS `(?<![\w-])`; the `regex` crate has
@@ -908,6 +940,8 @@ pub static REGEX_MATCHERS: Lazy<Vec<Matcher>> = Lazy::new(|| {
}
let scope = containing_markup_tag(line)(m.index);
find_solid_chromatic_bg(&scope).is_some()
&& stripe_child_markup_empty(line, m.index)
&& STRIPE_CHILD_CUE_RE.is_match(&scope)
&& !scope_has_fixed_height(&scope)
&& !STRIPE_CHILD_ROUNDED_FULL_RE.is_match(&scope)
&& !STRIPE_CHILD_ARIA_RE.is_match(&scope)
@@ -1495,11 +1529,11 @@ mod tests {
vec!["w-1 + bg-amber-500 stripe child"]
);
assert_eq!(
s(r#"<div class="w-[4px] bg-blue-500"></div>"#),
s(r#"<div class="w-[4px] bg-blue-500 shrink-0"></div>"#),
vec!["w-[4px] + bg-blue-500 stripe child"]
);
assert_eq!(
s(r#"<span className="w-0.5 bg-rose-500" />"#),
s(r#"<span className="w-0.5 bg-rose-500 shrink-0" />"#),
vec!["w-0.5 + bg-rose-500 stripe child"]
);
assert_eq!(
@@ -1517,6 +1551,8 @@ mod tests {
r#"<div className="w-1 shrink-0"><span className="bg-amber-500" /></div>"#
)
.is_empty());
assert!(s(r#"<div className="w-1 bg-amber-500">|</div>"#).is_empty());
assert!(s(r#"<div className="w-1 bg-amber-500" />"#).is_empty());
}
#[test]
+13 -12
View File
@@ -9,11 +9,10 @@ use crate::background::{
a_ge, a_gt, read_own_background_color, resolve_background, resolve_background_info,
resolve_border_radius_px, resolve_gradient_stops, sv, sv_opt, CustomPropMap,
};
use crate::cascade::{
expand_static_box_values, split_css_tokens, StyleValues,
};
use crate::cascade::{expand_static_box_values, split_css_tokens, StyleValues};
use crate::dom::{StaticDocument, StaticElement};
use crate::quality::{collapse_ws, pf0, resolve_font_size_px};
use impeccable_core::checks::css_scan::css_length_to_px;
use impeccable_core::checks::measures::{
self, border_colors_from_style, border_widths_from_style, check_gpt_thin_border_wide_shadow,
check_oversized_h1, check_radial_spotlight, positioned_style_implies_escape, resolve_length_px,
@@ -513,14 +512,14 @@ pub fn check_element_stripe_child(el: &StaticElement<'_>, style: &StyleValues) -
return Vec::new();
}
let width = pf0(sv(style, "width"));
let width = css_length_to_px(sv(style, "width")).unwrap_or_else(|| pf0(sv(style, "width")));
let position = js::to_lower_case(sv(style, "position"));
let host_style = host.style();
let edge = if position == "absolute" || position == "fixed" {
let height_raw = sv(style, "height");
let inset = static_resolved_inset(style);
let height_stretches = height_raw == "100%"
|| (static_edge_hugs(&inset[0]) && static_edge_hugs(&inset[2]));
let height_stretches =
height_raw == "100%" || (static_edge_hugs(&inset[0]) && static_edge_hugs(&inset[2]));
if !height_stretches {
return Vec::new();
}
@@ -546,11 +545,12 @@ pub fn check_element_stripe_child(el: &StaticElement<'_>, style: &StyleValues) -
} else {
sv(host_style, "alignItems")
};
let is_stretch =
effective_align.is_empty() || effective_align == "stretch" || effective_align == "normal";
let is_stretch = effective_align.is_empty()
|| effective_align == "stretch"
|| effective_align == "normal";
let height_raw = sv(style, "height");
let height_stretches = (height_raw.is_empty() || height_raw == "auto" || height_raw == "100%")
&& is_stretch;
let height_stretches =
height_raw == "100%" || ((height_raw.is_empty() || height_raw == "auto") && is_stretch);
if !height_stretches {
return Vec::new();
}
@@ -558,10 +558,11 @@ pub fn check_element_stripe_child(el: &StaticElement<'_>, style: &StyleValues) -
if siblings.len() < 2 {
return Vec::new();
}
let reverse = pdir.contains("reverse");
if siblings.first() == Some(el) {
Some("left")
Some(if reverse { "right" } else { "left" })
} else if siblings.last() == Some(el) {
Some("right")
Some(if reverse { "left" } else { "right" })
} else {
None
}
-3
View File
@@ -117,9 +117,6 @@ pub const STATIC_DEFAULT_STYLE: &[(&str, &str)] = &[
("left", "auto"),
("inset", ""),
("display", ""),
("flexDirection", "row"),
("alignItems", "stretch"),
("alignSelf", "auto"),
("overflow", "visible"),
("overflowX", "visible"),
("overflowY", "visible"),
+33 -2
View File
@@ -65,7 +65,8 @@
//! parse in the same process).
use super::csstree::{self, Important, Node};
use super::shorthand::expand_static_declaration;
use super::shorthand::{expand_static_box_values, expand_static_declaration, Expanded};
use super::values::split_css_tokens;
use impeccable_core::js;
use indexmap::IndexMap;
use once_cell::sync::Lazy;
@@ -177,6 +178,32 @@ impl<K: Hash + Eq> SpecifiedStore<K> {
}
}
/// Layout properties the stripe-child static adapter needs that are not in
/// the frozen `expandStaticDeclaration` allowlist. Applied here so the
/// recorded vectors stay byte-equal.
fn extra_specified_expansions(prop: &str, value: &str) -> Vec<Expanded> {
let p = js::to_lower_case(prop);
let v = js::trim(value);
if v.is_empty() {
return Vec::new();
}
match p.as_str() {
"flex-direction" => vec![("flexDirection".into(), v.to_string())],
"align-items" => vec![("alignItems".into(), v.to_string())],
"align-self" => vec![("alignSelf".into(), v.to_string())],
"inset" => {
let vals = expand_static_box_values(&split_css_tokens(v));
vec![
("top".into(), vals[0].clone()),
("right".into(), vals[1].clone()),
("bottom".into(), vals[2].clone()),
("left".into(), vals[3].clone()),
]
}
_ => Vec::new(),
}
}
/// JS: css-cascade.mjs#applyStaticDeclaration(specified, node, prop, value, meta)
pub fn apply_static_declaration<K: Hash + Eq>(
specified: &mut SpecifiedStore<K>,
@@ -186,7 +213,11 @@ pub fn apply_static_declaration<K: Hash + Eq>(
meta: &DeclMeta,
) {
let map = specified.map.entry(node).or_default();
for (expanded_prop, expanded_value) in expand_static_declaration(prop, value) {
let extra = extra_specified_expansions(prop, value);
for (expanded_prop, expanded_value) in expand_static_declaration(prop, value)
.into_iter()
.chain(extra)
{
let existing = map.get(&expanded_prop).map(|d| &d.meta);
if compare_static_priority(existing, meta) {
let next = SpecifiedDecl {
+63 -5
View File
@@ -2,11 +2,15 @@ use impeccable_html::{detect_html_source, DetectHtmlOptions};
use std::path::Path;
fn side_tab_snippets(html: &str) -> Vec<String> {
detect_html_source(html, Path::new("/app/stripe.html"), &DetectHtmlOptions::default())
.into_iter()
.filter(|f| f.antipattern == "side-tab")
.map(|f| f.snippet)
.collect()
detect_html_source(
html,
Path::new("/app/stripe.html"),
&DetectHtmlOptions::default(),
)
.into_iter()
.filter(|f| f.antipattern == "side-tab")
.map(|f| f.snippet)
.collect()
}
#[test]
@@ -46,6 +50,7 @@ fn absolute_top_bottom_flags() {
</body></html>"#;
let hits = side_tab_snippets(html);
assert_eq!(hits.len(), 1);
assert!(hits[0].contains("stripe child (left)"));
}
#[test]
@@ -107,3 +112,56 @@ fn neutral_and_contentful_and_wide_do_not_flag() {
</body></html>"#;
assert!(side_tab_snippets(wide).is_empty());
}
#[test]
fn rem_width_flags() {
let html = r#"<!DOCTYPE html><html><head><style>
.card { display: flex; width: 320px; height: 100px; }
.stripe { width: 0.25rem; background: #f59e0b; }
</style></head><body>
<div class="card"><div class="stripe"></div><div>Body</div></div>
</body></html>"#;
let hits = side_tab_snippets(html);
assert_eq!(hits.len(), 1);
assert!(hits[0].contains("stripe child (left)"));
}
#[test]
fn height_full_with_align_center_flags() {
let html = r#"<!DOCTYPE html><html><head><style>
.card { display: flex; align-items: center; width: 320px; height: 100px; }
.stripe { width: 4px; height: 100%; background: #f59e0b; }
</style></head><body>
<div class="card"><div class="stripe"></div><div>Body</div></div>
</body></html>"#;
let hits = side_tab_snippets(html);
assert_eq!(hits.len(), 1);
assert!(hits[0].contains("stripe child (left)"));
}
#[test]
fn inset_after_left_longhand_flags() {
let html = r#"<!DOCTYPE html><html><head><style>
.card { position: relative; width: 320px; height: 100px; }
.stripe { position: absolute; left: 10px; inset: 0 auto 0 0; width: 4px; background: #3b82f6; }
</style></head><body>
<div class="card"><div class="stripe"></div></div>
</body></html>"#;
let hits = side_tab_snippets(html);
assert_eq!(hits.len(), 1);
assert!(hits[0].contains("stripe child (left)"));
}
#[test]
fn row_reverse_first_child_is_right() {
let html = r#"<!DOCTYPE html><html><head><style>
.card { display: flex; flex-direction: row-reverse; width: 320px; height: 100px; }
.stripe { width: 4px; background: #f59e0b; }
.body { flex: 1; }
</style></head><body>
<div class="card"><div class="stripe"></div><div class="body">Content</div></div>
</body></html>"#;
let hits = side_tab_snippets(html);
assert_eq!(hits.len(), 1);
assert!(hits[0].contains("stripe child (right)"));
}
+1 -1
View File
@@ -332,7 +332,7 @@ Optional keys added later by engines (appended after the above): `ignoreValue` (
#### Static and regex engines (only what affects the contract)
- `detectHtml`: reads file, imports `htmlparser2`, `css-select`, `css-tree`, `domutils`; on import failure prints once to stderr `impeccable detect: DEGRADED - HTML parser modules unavailable (htmlparser2, css-select, css-tree, domutils).\nFalling back to regex matching. Custom properties, selector matching and computed contrast are NOT evaluated; findings are an undercount, not a clean bill of health.\n` and falls back to `detectText`. Inlines `<link rel=stylesheet href>` that are local (not `/^(https?:)?\/\//i`), query/hash stripped. Runs element rules, design-system rules (`checkSourceDesignSystem` + `collectStaticDesignSystemFindings`, merged), then page rules only when `isFullPage(html)` (`/<!doctype\s|<html[\s>]|<head[\s>]/i` after stripping comments), plus text-content analyzers; ends with inline-ignore filtering.
- `detectText`: regex line matchers (ids: side-tab including Tailwind stripe-child `w-*` + chromatic `bg-*` utilities, border-accent-on-rounded, overused-font, gradient-text, ai-color-palette, gray-on-color, bounce-easing, layout-transition, broken-image), inset-stripe/pseudo-stripe CSS scans, `codex-grid-background`, `<style>` blocks (Astro/Vue/Svelte), CSS-in-JS templates, design-system source checks; dedupe (same antipattern+snippet within 2 lines); page analyzers only when `isFullPage` and ext ∈ `{'.html','.htm','.astro','.vue','.svelte'}` or no ext (`<stdin>`): flat-type-hierarchy, monotonous-spacing, em-dash-overuse, marketing-buzzword, aphoristic-cadence, dark-glow (+ radial-halo, marquee); inline ignores last.
- `detectText`: regex line matchers (ids: side-tab including Tailwind stripe-child empty `w-*` + chromatic `bg-*` plus a `shrink-0` / edge-round cue, border-accent-on-rounded, overused-font, gradient-text, ai-color-palette, gray-on-color, bounce-easing, layout-transition, broken-image), inset-stripe/pseudo-stripe CSS scans, `codex-grid-background`, `<style>` blocks (Astro/Vue/Svelte), CSS-in-JS templates, design-system source checks; dedupe (same antipattern+snippet within 2 lines); page analyzers only when `isFullPage` and ext ∈ `{'.html','.htm','.astro','.vue','.svelte'}` or no ext (`<stdin>`): flat-type-hierarchy, monotonous-spacing, em-dash-overuse, marketing-buzzword, aphoristic-cadence, dark-glow (+ radial-halo, marquee); inline ignores last.
#### Profiler (`cli/engine/profile/profiler.mjs`)