mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 06:06:37 +03:00
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:
co-authored by
Claude Fable 5.1
parent
0f115e5d37
commit
2422ec058f
@@ -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: ®ex::Captures| {
|
||||
let whole = caps.get(0).map(|m| m.as_str()).unwrap_or("");
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user