From 6233ba2e89afdcfa5dbb5cdd028db329525c7bd6 Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Sun, 20 Sep 2026 19:39:48 -0700 Subject: [PATCH] A heading names both sides only when something joins them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `DESIGN_BOTH_SIDES_HEADING` asked for a `do` within twelve characters of a `don't`, which "Don't do this" and "What we don't do" both satisfy while meaning only the one thing. A section headed either of those stopped condemning the classes it names, so its counter-examples became declared components and the rules stood down on exactly the misuse the heading introduces. The two sides have to be joined by something that pairs them now — and, or, a slash, a comma — which is what "Dos and Don'ts" and "Do / Do not" have and a sentence that happens to contain both words does not. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01LQBUunp8QttxZqihybNmtL --- crates/detect/src/design_system.rs | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/crates/detect/src/design_system.rs b/crates/detect/src/design_system.rs index 0ef8d8d9f..ad2492b64 100644 --- a/crates/detect/src/design_system.rs +++ b/crates/detect/src/design_system.rs @@ -78,11 +78,16 @@ re!( ); // A heading that names both sides — "Do and Don't", "Dos and Don'ts", // "Do / Do not" — introduces a section of both, so the subsections under it -// say which is which and the heading itself condemns nothing. -re!( - DESIGN_BOTH_SIDES_HEADING, - r"(?i)\bdos?\b[^\n]{0,12}?\b(?:do ?n[o']?ts?|do not)\b|\b(?:do ?n[o']?ts?|do not)\b[^\n]{0,12}?\bdos?\b".to_string() -); +// say which is which and the heading itself condemns nothing. The two sides +// have to be *joined* by something that pairs them: "Don't do this" and "What +// we don't do" also put a `do` beside a `don't`, and they mean only the one +// thing. +re!(DESIGN_BOTH_SIDES_HEADING, { + let joiner = format!(r"{WS}*(?:and|or|&|/|\||\+|,|vs\.?|versus){WS}*", WS = WS); + let affirmative = r"\bdo'?s?\b"; + let negative = r"\b(?:do ?n[o']?ts?|do not)\b"; + format!("(?i)(?:{affirmative}{joiner}{negative}|{negative}{joiner}{affirmative})") +}); re!( FONT_SIZE_LITERAL_RE, format!("^-?[{D}.]+(?:px|rem)$", D = "0-9") @@ -2161,6 +2166,13 @@ mod tests { ### Don't\n\n- Reach for `.eyebrow`.\n"; assert_eq!(declared_component_selectors(md), vec![".kicker"]); + // A heading that only puts a `do` beside a `don't` is not a section + // of both, and still condemns what it names. + let md = "## Don't do this\n\n- `.eyebrow` above a heading.\n\n\ + ## What we don't do\n\n- `.tagline`\n\n\ + ## Components\n\n- `.kicker`\n"; + assert_eq!(declared_component_selectors(md), vec![".kicker"]); + // What the negative words govern is their own clause. "No ALL CAPS // outside the `.eyebrow` class" declares `.eyebrow`, and "four kinds // and no more" declares all four.