Sampled contrast: keep the sheet url rewrite inside one url token

The linked-sheet url rewrite ran one regex over the raw stylesheet text,
and its unquoted `url(` form could match across lines to a `)` in a
later rule, so an unclosed `url(` inside a comment would have replaced
the rules between them with one rewritten path, and that text feeds the
whole cascade, not only sampled contrast. Comments are now copied
through untouched, a quoted url ends at its line, and an unquoted one
stops at whitespace, quotes, parens, braces, or a semicolon, as CSS
does. Tests pin a commented `url(`, an unclosed quote, and an
unterminated comment.

AI assistance: Claude Code (Claude Fable 5.1), on maintainer instruction.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Abdul Wahab
2026-09-10 10:52:08 +05:00
co-authored by Claude Fable 5.1
parent 0f115e5d37
commit 2422ec058f
2 changed files with 47 additions and 4 deletions
+26 -3
View File
@@ -141,9 +141,14 @@ pub fn collect_static_css_text(
style_texts.join("\n")
}
// A quoted url cannot span a line and an unquoted one cannot hold
// whitespace, quotes, parens, braces, or semicolons (CSS syntax), so a
// stray `url(` can never pair with a `)` in a later rule.
static CSS_URL_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r#"(?i)url\(\s*(?:"((?:[^"\\]|\\.)*)"|'((?:[^'\\]|\\.)*)'|([^)"']*?))\s*\)"#)
.expect("CSS_URL_RE")
Regex::new(
r#"(?i)url\(\s*(?:"((?:[^"\\\n\r]|\\.)*)"|'((?:[^'\\\n\r]|\\.)*)'|([^)"'(\s{};]*))\s*\)"#,
)
.expect("CSS_URL_RE")
});
static URL_SCHEME_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^[A-Za-z][A-Za-z0-9+.-]*:").expect("URL_SCHEME_RE"));
@@ -153,8 +158,26 @@ static URL_SCHEME_RE: Lazy<Regex> =
/// directory has its relative urls rewritten to page-relative form here.
/// This is what lets the sampled-contrast path (#560) resolve the image the
/// winning declaration named. Root-relative, remote, `data:`, fragment, and
/// escaped urls are left as they are.
/// escaped urls are left as they are, and comments are copied through
/// untouched, so nothing inside one can reach the rules after it.
pub fn rewrite_sheet_urls(css: &str, sheet_dir: &str, page_dir: &str) -> String {
let mut out = String::with_capacity(css.len());
let mut rest = css;
while let Some(start) = rest.find("/*") {
out.push_str(&rewrite_code_urls(&rest[..start], sheet_dir, page_dir));
// An unclosed comment runs to the end of the sheet, as in CSS.
let end = rest[start + 2..]
.find("*/")
.map(|i| start + 2 + i + 2)
.unwrap_or(rest.len());
out.push_str(&rest[start..end]);
rest = &rest[end..];
}
out.push_str(&rewrite_code_urls(rest, sheet_dir, page_dir));
out
}
fn rewrite_code_urls(css: &str, sheet_dir: &str, page_dir: &str) -> String {
CSS_URL_RE
.replace_all(css, |caps: &regex::Captures| {
let whole = caps.get(0).map(|m| m.as_str()).unwrap_or("");
+21 -1
View File
@@ -360,7 +360,8 @@ fn linked_sheet_urls_are_rewritten_page_relative() {
);
assert!(out.contains("url(https://cdn.example.com/a.png)"), "{out}");
assert!(out.contains("url(#clip)"), "{out}");
assert!(out.contains("url(\"css/a b.png\")"), "{out}");
// An unquoted url with a space is not a url token in CSS; it stays put.
assert!(out.contains("URL( a b.png )"), "{out}");
// A sheet beside the page keeps every path where it was (the engine
// does not even call the rewrite for that case).
let same = rewrite_sheet_urls(css, "/site", "/site");
@@ -369,4 +370,23 @@ fn linked_sheet_urls_are_rewritten_page_relative() {
// A sheet above the page walks back up.
let up = rewrite_sheet_urls(".a { background: url(light.png) }", "/site", "/site/pages");
assert_eq!(up, ".a { background: url(../light.png) }");
// A stray `url(` in a comment, or an unclosed quote, never swallows the
// rules after it: comments pass through untouched and a url form ends
// where CSS says it ends.
let hazards = concat!(
"/* see url( for details */ .g { color: red }\n",
".h { background: url(a.png) }\n",
".i { background: url(\"oops }\n",
".j { background: url(b.png) }\n",
"/* url(unterminated.png",
);
let out = rewrite_sheet_urls(hazards, "/site/css", "/site");
assert!(
out.contains("/* see url( for details */ .g { color: red }"),
"{out}"
);
assert!(out.contains(".h { background: url(css/a.png) }"), "{out}");
assert!(out.contains(".i { background: url(\"oops }"), "{out}");
assert!(out.contains(".j { background: url(css/b.png) }"), "{out}");
assert!(out.ends_with("/* url(unterminated.png"), "{out}");
}