mirror of
https://github.com/lexmount/moli.git
synced 2026-09-27 16:01:31 +00:00
452 lines
16 KiB
Rust
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"));
|
|
}
|
|
}
|