Files
moli/moli-css-parse/src/util.rs
T
2026-08-11 00:10:12 +08:00

452 lines
16 KiB
Rust

use cssparser::serialize_identifier;
pub fn canonical_style_property_name(property: &str) -> String {
let property = property.trim();
if property.starts_with("--") {
return property.to_owned();
}
if property == "cssFloat" {
return "float".to_owned();
}
let lowered = property.to_ascii_lowercase();
match lowered.as_str() {
"color-adjust" => "print-color-adjust".to_owned(),
"-webkit-align-content" => "align-content".to_owned(),
"-webkit-align-items" => "align-items".to_owned(),
"-webkit-align-self" => "align-self".to_owned(),
"-webkit-animation" => "animation".to_owned(),
"-webkit-animation-delay" => "animation-delay".to_owned(),
"-webkit-animation-direction" => "animation-direction".to_owned(),
"-webkit-animation-duration" => "animation-duration".to_owned(),
"-webkit-animation-fill-mode" => "animation-fill-mode".to_owned(),
"-webkit-animation-iteration-count" => "animation-iteration-count".to_owned(),
"-webkit-animation-name" => "animation-name".to_owned(),
"-webkit-animation-play-state" => "animation-play-state".to_owned(),
"-webkit-animation-timing-function" => "animation-timing-function".to_owned(),
"-webkit-appearance" => "appearance".to_owned(),
"-webkit-backface-visibility" => "backface-visibility".to_owned(),
"-webkit-background-clip" => "background-clip".to_owned(),
"-webkit-background-origin" => "background-origin".to_owned(),
"-webkit-background-size" => "background-size".to_owned(),
"-webkit-box-shadow" => "box-shadow".to_owned(),
"-webkit-border-radius" => "border-radius".to_owned(),
"-webkit-border-top-left-radius" => "border-top-left-radius".to_owned(),
"-webkit-border-top-right-radius" => "border-top-right-radius".to_owned(),
"-webkit-border-bottom-right-radius" => "border-bottom-right-radius".to_owned(),
"-webkit-border-bottom-left-radius" => "border-bottom-left-radius".to_owned(),
"-webkit-box-sizing" => "box-sizing".to_owned(),
"-webkit-flex" => "flex".to_owned(),
"-webkit-flex-basis" => "flex-basis".to_owned(),
"-webkit-flex-direction" => "flex-direction".to_owned(),
"-webkit-flex-flow" => "flex-flow".to_owned(),
"-webkit-flex-grow" => "flex-grow".to_owned(),
"-webkit-flex-shrink" => "flex-shrink".to_owned(),
"-webkit-flex-wrap" => "flex-wrap".to_owned(),
"-webkit-filter" => "filter".to_owned(),
"-webkit-justify-content" => "justify-content".to_owned(),
"-webkit-order" => "order".to_owned(),
"-webkit-perspective" => "perspective".to_owned(),
"-webkit-perspective-origin" => "perspective-origin".to_owned(),
"-webkit-transform" => "transform".to_owned(),
"-webkit-transform-style" => "transform-style".to_owned(),
"-webkit-transition" => "transition".to_owned(),
"-webkit-transition-delay" => "transition-delay".to_owned(),
"-webkit-transition-duration" => "transition-duration".to_owned(),
"-webkit-transition-property" => "transition-property".to_owned(),
"-webkit-transition-timing-function" => "transition-timing-function".to_owned(),
"-webkit-user-select" => "user-select".to_owned(),
_ => lowered,
}
}
pub fn is_cssom_custom_property_name(property: &str) -> bool {
property.starts_with("--") && property.len() > 2 && !property.chars().any(char::is_whitespace)
}
pub fn serialize_style_property_name(property: &str) -> String {
if !property.starts_with("--") {
return property.to_owned();
}
let mut output = String::new();
serialize_identifier(property, &mut output).unwrap_or(());
output
}
pub fn canonical_style_property_identifier(property: &str) -> String {
let property = property.trim();
if property.starts_with("--") {
return property.to_owned();
}
if property == "cssFloat" {
return "float".to_owned();
}
for prefix in ["WebKit", "Webkit", "webkit"] {
if let Some(rest) = property.strip_prefix(prefix)
&& rest
.chars()
.next()
.is_some_and(|ch| ch.is_ascii_uppercase())
{
let property = format!("-webkit-{}", camel_to_kebab(&decapitalize_ascii_head(rest)));
return canonical_style_property_name(&property);
}
}
let lowered = if property.contains('-') {
property.to_ascii_lowercase()
} else {
camel_to_kebab(property).to_ascii_lowercase()
};
canonical_style_property_name(&lowered)
}
pub fn camel_case_style_property_name(name: &str) -> Option<String> {
if name.starts_with("--") {
return None;
}
if name == "float" {
return Some("cssFloat".to_owned());
}
let mut output = String::with_capacity(name.len());
let mut uppercase_next = false;
for ch in name.chars() {
if ch == '-' {
uppercase_next = true;
continue;
}
if uppercase_next {
output.push(ch.to_ascii_uppercase());
uppercase_next = false;
} else {
output.push(ch);
}
}
(!output.is_empty()).then_some(output)
}
pub fn decapitalize_ascii_head(input: &str) -> String {
let mut chars = input.chars();
let Some(first) = chars.next() else {
return String::new();
};
let mut out = String::new();
out.push(first.to_ascii_lowercase());
out.extend(chars);
out
}
pub fn camel_to_kebab(property: &str) -> String {
let mut out = String::new();
for ch in property.chars() {
if ch.is_ascii_uppercase() {
if !out.is_empty() {
out.push('-');
}
out.push(ch.to_ascii_lowercase());
} else {
out.push(ch);
}
}
out
}
pub fn split_important_priority(value: &str) -> (String, bool) {
let trimmed = value.trim();
let lower = trimmed.to_ascii_lowercase();
let Some(important_start) = lower.rfind("important") else {
return (trimmed.to_owned(), false);
};
if important_start + "important".len() != lower.len() {
return (trimmed.to_owned(), false);
}
let before_important = &trimmed[..important_start];
let before_important_trimmed = before_important.trim_end();
if let Some(without_bang) = before_important_trimmed.strip_suffix('!') {
return (without_bang.trim_end().to_owned(), true);
}
(trimmed.to_owned(), false)
}
pub fn unquote_css_string(value: &str) -> String {
let trimmed = value.trim();
if trimmed.len() >= 2 {
let bytes = trimmed.as_bytes();
if (bytes[0] == b'"' && bytes[trimmed.len() - 1] == b'"')
|| (bytes[0] == b'\'' && bytes[trimmed.len() - 1] == b'\'')
{
return trimmed[1..trimmed.len() - 1].to_owned();
}
}
trimmed.to_owned()
}
pub fn escape_top_level_semicolons(value: &str) -> String {
let mut output = String::with_capacity(value.len());
let mut depth = 0usize;
let mut quote = None;
let mut escaped = false;
for ch in value.chars() {
if escaped {
output.push(ch);
escaped = false;
continue;
}
match ch {
'\\' => {
output.push(ch);
escaped = true;
}
'"' | '\'' => {
if quote == Some(ch) {
quote = None;
} else if quote.is_none() {
quote = Some(ch);
}
output.push(ch);
}
'(' if quote.is_none() => {
depth += 1;
output.push(ch);
}
')' if quote.is_none() => {
depth = depth.saturating_sub(1);
output.push(ch);
}
';' if quote.is_none() && depth == 0 => {
output.push('\\');
output.push(ch);
}
_ => output.push(ch),
}
}
output
}
pub fn unescape_top_level_semicolons(value: &str) -> String {
let mut output = String::with_capacity(value.len());
let mut chars = value.chars().peekable();
let mut depth = 0usize;
let mut quote = None;
let mut escaped = false;
while let Some(ch) = chars.next() {
if escaped {
output.push(ch);
escaped = false;
continue;
}
match ch {
'\\' if quote.is_none() && depth == 0 && chars.peek() == Some(&';') => {
let _ = chars.next();
output.push(';');
}
'\\' => {
output.push(ch);
escaped = true;
}
'"' | '\'' => {
if quote == Some(ch) {
quote = None;
} else if quote.is_none() {
quote = Some(ch);
}
output.push(ch);
}
'(' if quote.is_none() => {
depth += 1;
output.push(ch);
}
')' if quote.is_none() => {
depth = depth.saturating_sub(1);
output.push(ch);
}
_ => output.push(ch),
}
}
output
}
#[cfg(test)]
mod tests {
use super::{
camel_case_style_property_name, camel_to_kebab, canonical_style_property_identifier,
decapitalize_ascii_head, escape_top_level_semicolons, is_cssom_custom_property_name,
serialize_style_property_name, unescape_top_level_semicolons,
};
#[test]
fn semicolon_round_trip() {
let escaped = escape_top_level_semicolons("Hello; world!");
assert_eq!(escaped, "Hello\\; world!");
let back = unescape_top_level_semicolons(&escaped);
assert_eq!(back, "Hello; world!");
}
#[test]
fn style_property_identifier_matches_renderer_surface() {
assert_eq!(canonical_style_property_identifier(" cssFloat "), "float");
assert_eq!(
canonical_style_property_identifier("backgroundColor"),
"background-color"
);
assert_eq!(
canonical_style_property_identifier("colorAdjust"),
"print-color-adjust"
);
assert_eq!(
canonical_style_property_identifier("WebkitTransition"),
"transition"
);
assert_eq!(
canonical_style_property_identifier("webkitAlignContent"),
"align-content"
);
assert_eq!(
canonical_style_property_identifier("-webkit-align-items"),
"align-items"
);
assert_eq!(
canonical_style_property_identifier("webkitBackgroundSize"),
"background-size"
);
assert_eq!(
canonical_style_property_identifier("webkitBackfaceVisibility"),
"backface-visibility"
);
assert_eq!(
canonical_style_property_identifier("webkitBackgroundOrigin"),
"background-origin"
);
assert_eq!(
canonical_style_property_identifier("webkitBoxShadow"),
"box-shadow"
);
assert_eq!(
canonical_style_property_identifier("webkitBorderRadius"),
"border-radius"
);
assert_eq!(
canonical_style_property_identifier("-webkit-border-radius"),
"border-radius"
);
assert_eq!(
canonical_style_property_identifier("webkitBorderTopLeftRadius"),
"border-top-left-radius"
);
assert_eq!(
canonical_style_property_identifier("-webkit-border-top-right-radius"),
"border-top-right-radius"
);
assert_eq!(
canonical_style_property_identifier("webkitBorderBottomRightRadius"),
"border-bottom-right-radius"
);
assert_eq!(
canonical_style_property_identifier("-webkit-border-bottom-left-radius"),
"border-bottom-left-radius"
);
assert_eq!(
canonical_style_property_identifier("webkitTransform"),
"transform"
);
assert_eq!(
canonical_style_property_identifier("webkitFilter"),
"filter"
);
assert_eq!(
canonical_style_property_identifier("-webkit-filter"),
"filter"
);
assert_eq!(
canonical_style_property_identifier("webkitPerspective"),
"perspective"
);
assert_eq!(
canonical_style_property_identifier("-webkit-perspective"),
"perspective"
);
assert_eq!(
canonical_style_property_identifier("webkitPerspectiveOrigin"),
"perspective-origin"
);
assert_eq!(
canonical_style_property_identifier("-webkit-perspective-origin"),
"perspective-origin"
);
assert_eq!(
canonical_style_property_identifier("-Webkit-Box-Sizing"),
"box-sizing"
);
assert_eq!(canonical_style_property_identifier("webkitFlex"), "flex");
assert_eq!(
canonical_style_property_identifier("webkitFlexFlow"),
"flex-flow"
);
assert_eq!(
canonical_style_property_identifier("-webkit-flex-basis"),
"flex-basis"
);
assert_eq!(
canonical_style_property_identifier("webkitFlexDirection"),
"flex-direction"
);
assert_eq!(
canonical_style_property_identifier("-webkit-justify-content"),
"justify-content"
);
assert_eq!(canonical_style_property_identifier("webkitOrder"), "order");
assert_eq!(
canonical_style_property_identifier("webkitTransitionDuration"),
"transition-duration"
);
assert_eq!(
canonical_style_property_identifier("-webkit-animation"),
"animation"
);
assert_eq!(
canonical_style_property_identifier("webkitAnimationTimingFunction"),
"animation-timing-function"
);
assert_eq!(
canonical_style_property_identifier("webkitTransformStyle"),
"transform-style"
);
assert_eq!(canonical_style_property_identifier("--xY"), "--xY");
}
#[test]
fn style_property_identifier_helpers_convert_names() {
assert_eq!(
camel_case_style_property_name("float").as_deref(),
Some("cssFloat")
);
assert_eq!(
camel_case_style_property_name("-webkit-transition").as_deref(),
Some("WebkitTransition")
);
assert_eq!(camel_case_style_property_name("--custom"), None);
assert_eq!(camel_to_kebab("WebkitTransition"), "webkit-transition");
assert_eq!(decapitalize_ascii_head("Transition"), "transition");
assert_eq!(decapitalize_ascii_head(""), "");
}
#[test]
fn custom_property_names_serialize_as_css_identifiers() {
assert_eq!(serialize_style_property_name("color"), "color");
assert_eq!(serialize_style_property_name("--a;b"), r#"--a\;b"#);
assert_eq!(serialize_style_property_name(r#"--\"#), r#"--\\"#);
assert_eq!(serialize_style_property_name("--ab"), "--ab");
}
#[test]
fn cssom_custom_property_name_requires_nonempty_suffix_without_whitespace() {
assert!(is_cssom_custom_property_name("--x"));
assert!(is_cssom_custom_property_name("---"));
assert!(is_cssom_custom_property_name("--a;b"));
assert!(is_cssom_custom_property_name("--\\"));
assert!(!is_cssom_custom_property_name("--"));
assert!(!is_cssom_custom_property_name("--x "));
assert!(!is_cssom_custom_property_name("--x y"));
assert!(!is_cssom_custom_property_name(" --x"));
}
}