mod colors;
mod numbers;
mod splitting;
pub(crate) use colors::format_color_from_source;
pub(crate) use numbers::normalize_dimension_from_source;
pub(crate) use splitting::{
collapse_whitespace_runs, has_closing_comma, normalize_css_whitespace, normalize_is_noop_in,
split_args_by_comma, split_by_space_preserving_parens,
};
use std::borrow::Cow;
use super::boundary_ws::boundary_run_spelling;
use crate::color::is_hex_color_body;
use crate::escapes::escape_span_at;
use numbers::{canonical_unit, is_known_css_unit, normalize_css_number};
use tsv_lang::printing::format_string_literal;
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum ValueReader {
Value,
MediaQuery,
}
pub(crate) fn normalize_value_text(input: &str, path: ValueReader) -> String {
let bytes = input.as_bytes();
let mut out = String::with_capacity(input.len());
let mut i = 0;
let mut paren_depth = 0usize;
let mut preserve_from: Option<usize> = None;
let mut prev_ident_preserve_fn = false;
let mut prev_run_absorbs_digit = false;
let mut media_name_until = 0usize;
while i < bytes.len() {
let b = bytes[i];
let after_preserve_ident = prev_ident_preserve_fn;
prev_ident_preserve_fn = false;
let after_absorbing_run = prev_run_absorbs_digit;
prev_run_absorbs_digit = false;
if let Some(end) = escape_span_at(input, i) {
out.push_str(&input[i..end]);
i = end;
continue;
}
if b == b'"' || b == b'\'' {
let start = i;
let closed = crate::lexer::string_end(bytes, start).ok();
i = closed.unwrap_or(input.len());
let literal = &input[start..i];
if closed.is_some() {
let content = &literal[1..literal.len() - 1];
out.push_str(&format_string_literal(content, b as char));
} else {
out.push_str(literal);
}
continue;
}
if crate::comments::is_comment_start(bytes, i) {
let start = i;
i = crate::comments::comment_end(input.as_bytes(), i);
out.push_str(&input[start..i]);
continue;
}
let Some(ch) = input[i..].chars().next() else {
break;
};
if is_ident_start(ch) {
let start = i;
i += ch.len_utf8();
while let Some(c) = input[i..].chars().next() {
if is_ident_continue(c) {
i += c.len_utf8();
} else {
break;
}
}
let (emit_len, absorbs) = ident_run_split(&input[start..i], path);
i = start + emit_len;
let ident = &input[start..i];
out.push_str(ident);
let media_value_text = path == ValueReader::MediaQuery
&& media_node_at(paren_depth, i, media_name_until) == MediaNode::Value;
if ident.eq_ignore_ascii_case("url")
&& bytes.get(i) == Some(&b'(')
&& !url_arg_is_quoted(input, i)
&& !media_value_text
{
out.push_str(consume_paren_group(input, &mut i));
continue;
}
prev_ident_preserve_fn = ident.eq_ignore_ascii_case("selector");
prev_run_absorbs_digit = absorbs;
continue;
}
if b == b'#' && path == ValueReader::Value {
let start = i;
i += 1;
let body_start = i;
if let Some(c) = input[i..].chars().next()
&& is_css_ident_code_point(c)
{
i += c.len_utf8();
while let Some(c) = input[i..].chars().next() {
if is_hash_word_char(c) {
i += c.len_utf8();
} else {
break;
}
}
}
let body = &input[body_start..i];
if preserve_from.is_none() && is_hex_color_body(body) {
out.push('#');
for c in body.chars() {
out.push(c.to_ascii_lowercase());
}
} else {
out.push_str(&input[start..i]);
}
prev_run_absorbs_digit = !body.is_empty();
continue;
}
let num_len = crate::number::number_part_len(&input[i..]);
if num_len > 0 {
let num_start = i;
let num = &input[i..i + num_len];
i += num_len;
let unit_start = i;
while i < bytes.len() && bytes[i].is_ascii_alphabetic() {
i += 1;
}
let unit = &input[unit_start..i];
let normalizes = preserve_from.is_none()
&& match path {
ValueReader::Value => true,
ValueReader::MediaQuery => {
media_node_at(paren_depth, num_start, media_name_until) != MediaNode::Name
&& (unit.is_empty()
|| unit.eq_ignore_ascii_case("n")
|| is_known_css_unit(unit))
}
};
let normalized = normalizes
.then(|| normalize_css_number(num))
.filter(|n| !(after_absorbing_run && n.starts_with(is_css_ident_code_point)));
if let Some(normalized) = normalized {
out.push_str(&normalized);
out.push_str(&prelude_unit(unit, path));
} else {
out.push_str(num);
out.push_str(unit);
}
prev_run_absorbs_digit =
path == ValueReader::Value && out.ends_with(is_css_ident_code_point);
continue;
}
match ch {
'(' => {
if path == ValueReader::MediaQuery
&& paren_depth == 0
&& opens_feature_expression(bytes, i)
{
let (group_end, _) = scan_paren_group(input, i);
media_name_until = i + feature_name_end(&input[i..group_end]);
}
paren_depth += 1;
if after_preserve_ident && preserve_from.is_none() {
preserve_from = Some(paren_depth);
}
}
')' => {
if preserve_from == Some(paren_depth) {
preserve_from = None;
}
paren_depth = paren_depth.saturating_sub(1);
}
_ => {}
}
out.push(ch);
i += ch.len_utf8();
}
out
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum MediaNode {
Type,
Name,
Value,
}
fn media_node_at(paren_depth: usize, i: usize, media_name_until: usize) -> MediaNode {
if i < media_name_until {
MediaNode::Name
} else if paren_depth >= 1 {
MediaNode::Value
} else {
MediaNode::Type
}
}
fn prelude_unit(unit: &str, path: ValueReader) -> Cow<'_, str> {
match path {
ValueReader::Value => canonical_unit(unit),
ValueReader::MediaQuery if unit.bytes().any(|b| b.is_ascii_uppercase()) => {
Cow::Owned(unit.to_ascii_lowercase())
}
ValueReader::MediaQuery => Cow::Borrowed(unit),
}
}
fn url_arg_is_quoted(s: &str, open: usize) -> bool {
let bytes = s.as_bytes();
let mut j = open + 1;
while j < bytes.len() && bytes[j].is_ascii_whitespace() {
j += 1;
}
matches!(bytes.get(j), Some(b'"') | Some(b'\''))
}
fn consume_paren_group<'s>(s: &'s str, i: &mut usize) -> &'s str {
let bytes = s.as_bytes();
let start = *i;
let mut depth = 0usize;
while *i < s.len() {
let b = bytes[*i];
if let Some(end) = escape_span_at(s, *i) {
*i = end;
continue;
}
*i += 1;
if b == b'(' {
depth += 1;
} else if b == b')' {
depth -= 1;
if depth == 0 {
break;
}
}
}
&s[start..*i]
}
fn is_ident_start(ch: char) -> bool {
ch.is_alphabetic() || ch == '_' || ch == '$' || ch == '@' || !ch.is_ascii()
}
fn is_ident_continue(ch: char) -> bool {
is_ident_start(ch) || ch.is_ascii_digit() || ch == '-'
}
fn is_css_ident_code_point(ch: char) -> bool {
ch.is_ascii_alphanumeric()
|| ch == '-'
|| ch == '_'
|| crate::lexer::is_non_ascii_identifier_codepoint(ch)
}
fn is_hash_word_char(ch: char) -> bool {
is_css_ident_code_point(ch) || ch == '.' || ch == '/'
}
fn is_media_word_part_start(ch: char) -> bool {
is_ident_start(ch) && ch != '$' && ch != '@'
}
fn media_word_part_split(run: &str) -> (usize, bool) {
let tail_start = run
.char_indices()
.filter(|&(_, c)| c == '$' || c == '@')
.map(|(i, c)| i + c.len_utf8())
.next_back()
.unwrap_or(0);
let tail = &run[tail_start..];
if tail.chars().any(is_media_word_part_start) {
return (run.len(), true);
}
let emit_len = tail
.char_indices()
.find(|&(_, c)| c.is_ascii_digit())
.map_or(run.len(), |(i, _)| tail_start + i);
(emit_len, false)
}
fn ident_run_split(run: &str, path: ValueReader) -> (usize, bool) {
match path {
ValueReader::Value => (run.len(), run.ends_with(is_css_ident_code_point)),
ValueReader::MediaQuery => media_word_part_split(run),
}
}
fn ident_sequence_end(s: &str, start: usize) -> usize {
let bytes = s.as_bytes();
let mut i = start;
while i < s.len() {
if bytes[i] == b'\\' {
let Some(end) = escape_span_at(s, i) else {
break;
};
i = end;
continue;
}
let Some(ch) = s[i..].chars().next() else {
break;
};
if !is_ident_continue(ch) {
break;
}
i += ch.len_utf8();
}
i
}
pub(crate) fn lowercase_media_feature_names(query: &str) -> Cow<'_, str> {
let bytes = query.as_bytes();
if !bytes.iter().any(u8::is_ascii_uppercase) {
return Cow::Borrowed(query);
}
let mut out = String::with_capacity(query.len());
let mut i = 0;
while i < query.len() {
if let Some(end) = trivia_span_at(query, i) {
out.push_str(&query[i..end]);
i = end;
continue;
}
if let Some(end) = escape_span_at(query, i) {
out.push_str(&query[i..end]);
i = end;
continue;
}
match bytes[i] {
b'(' if opens_feature_expression(bytes, i) => {
let (end, has_nested) = scan_paren_group(query, i);
if has_nested {
out.push_str(&query[i..end]);
} else {
lowercase_simple_feature_expr(&query[i..end], &mut out);
}
i = end;
}
_ => {
let ch = query[i..].chars().next().unwrap_or('\0');
out.push(ch);
i += ch.len_utf8();
}
}
}
Cow::Owned(out)
}
fn skip_string(s: &str, start: usize) -> usize {
crate::lexer::string_end(s.as_bytes(), start).unwrap_or(s.len())
}
fn trivia_span_at(s: &str, i: usize) -> Option<usize> {
let bytes = s.as_bytes();
match bytes[i] {
b'/' if crate::comments::is_comment_start(bytes, i) => {
Some(crate::comments::comment_end(bytes, i))
}
b'"' | b'\'' => Some(skip_string(s, i)),
_ => None,
}
}
fn opens_feature_expression(bytes: &[u8], i: usize) -> bool {
let ends_ident = |b: u8| b.is_ascii_alphanumeric() || b == b'-' || b == b'_' || b >= 0x80;
i == 0 || !ends_ident(bytes[i - 1])
}
fn scan_paren_group(s: &str, open: usize) -> (usize, bool) {
let bytes = s.as_bytes();
let mut i = open + 1;
let mut depth = 1usize;
let mut has_nested = false;
while i < s.len() {
if let Some(end) = trivia_span_at(s, i) {
i = end;
continue;
}
if let Some(end) = escape_span_at(s, i) {
i = end;
continue;
}
match bytes[i] {
b'(' => {
if opens_feature_expression(bytes, i) {
has_nested = true;
}
depth += 1;
i += 1;
}
b')' => {
depth -= 1;
i += 1;
if depth == 0 {
return (i, has_nested);
}
}
_ => i += 1,
}
}
(s.len(), has_nested)
}
fn feature_name_end(group: &str) -> usize {
debug_assert!(
group.starts_with('('),
"a feature expression is its whole parenthesized group"
);
let bytes = group.as_bytes();
let mut i = 0;
let mut depth = 0usize;
while i < group.len() {
if let Some(end) = trivia_span_at(group, i) {
i = end;
continue;
}
if let Some(end) = escape_span_at(group, i) {
i = end;
continue;
}
match bytes[i] {
b'(' => depth += 1,
b')' => depth = depth.saturating_sub(1),
b':' if depth >= 1 => return i,
_ => {}
}
i += 1;
}
group.len()
}
fn feature_name_preserves_case(name: &str) -> bool {
name.starts_with("--")
|| name.starts_with('%')
|| name.contains(['$', '@', '#'])
|| (name.contains('(') && name.contains(')'))
}
fn lowercase_simple_feature_expr(group: &str, out: &mut String) {
let name_end = feature_name_end(group);
if feature_name_preserves_case(crate::escapes::trim_css(&group[1..name_end])) {
out.push_str(group);
return;
}
let mut i = 0;
while i < name_end {
if let Some(end) = trivia_span_at(group, i) {
out.push_str(&group[i..end]);
i = end;
continue;
}
let ch = group[i..].chars().next().unwrap_or('\0');
let end = if is_ident_start(ch) || ch == '-' || ch == '\\' {
ident_sequence_end(group, i)
} else {
i
};
if end > i {
out.push_str(&lowercase_feature_name(&group[i..end]));
i = end;
} else {
out.push(ch);
i += ch.len_utf8();
}
}
debug_assert_eq!(i, name_end, "the name walk overshot the separator");
out.push_str(&group[name_end..]);
}
fn lowercase_feature_name(name: &str) -> Cow<'_, str> {
if name.bytes().any(|b| b.is_ascii_uppercase()) {
Cow::Owned(name.to_ascii_lowercase())
} else {
Cow::Borrowed(name)
}
}
pub(crate) fn extract_property_name(
decl_source: &str,
colon_pos: usize,
has_block_comment: bool,
) -> Cow<'_, str> {
let property_part = &decl_source[..colon_pos];
if has_block_comment && let Some(comment_start) = property_part.find("/*") {
let before_comment = trim_property_part(&property_part[..comment_start]);
debug_assert!(property_part.starts_with(before_comment));
let mut out = String::from(before_comment);
let mut gap_start = before_comment.len();
let mut comment_pos = comment_start;
loop {
let rest = &property_part[comment_pos..];
let Some(comment_len) = crate::comments::comment_end_checked(rest.as_bytes(), 0) else {
return Cow::Borrowed(trim_property_part(property_part));
};
out.push_str(&boundary_run_spelling(
&property_part[gap_start..comment_pos],
));
out.push(' ');
out.push_str(&rest[..comment_len]);
gap_start = comment_pos + comment_len;
match rest[comment_len..].find("/*") {
Some(next) => comment_pos = gap_start + next,
None => break,
}
}
out.push_str(&boundary_run_spelling(&property_part[gap_start..]));
Cow::Owned(out)
} else {
let bare = trim_property_part(property_part);
if bare.len() == property_part.len() {
return Cow::Borrowed(bare);
}
debug_assert!(property_part.starts_with(bare));
let run = boundary_run_spelling(&property_part[bare.len()..]);
if !run.is_empty() {
Cow::Owned(format!("{bare}{run}"))
} else if crate::escapes::ends_with_live_hex_escape(bare) {
Cow::Owned(format!("{bare} "))
} else {
Cow::Borrowed(bare)
}
}
}
#[inline]
fn trim_property_part(property_part: &str) -> &str {
let bytes = property_part.as_bytes();
if let (Some(&first), Some(&last)) = (bytes.first(), bytes.last())
&& !crate::whitespace::byte_may_be_boundary_whitespace(first)
&& !crate::whitespace::byte_may_be_boundary_whitespace(last)
{
debug_assert_eq!(
trim_property_part_wide(property_part).len(),
property_part.len(),
"an ASCII non-whitespace byte at each end proves there is nothing to trim"
);
return property_part;
}
trim_property_part_wide(property_part)
}
#[inline(never)]
fn trim_property_part_wide(property_part: &str) -> &str {
let trimmed = property_part.trim_matches(crate::whitespace::is_boundary_whitespace);
if trimmed.len() == property_part.len()
|| !tsv_lang::printing::ends_with_open_escape(trimmed.as_bytes())
{
return trimmed;
}
debug_assert!(property_part.starts_with(trimmed));
let end = trimmed.len();
match property_part[end..].chars().next() {
Some(c) if !matches!(c, '\n' | '\r' | '\x0c') => &property_part[..end + c.len_utf8()],
_ => trimmed,
}
}
pub(crate) fn lowercase_property_name(name: Cow<'_, str>) -> Cow<'_, str> {
if !tsv_lang::swar::has_ascii_uppercase(name.as_bytes()) {
return name;
}
let standard_start = name
.as_bytes()
.first()
.is_some_and(|&b| b.is_ascii_alphabetic() || b == b'-');
if !standard_start || name.starts_with("--") || name.contains("/*") || name.contains('\\') {
return name;
}
Cow::Owned(name.to_ascii_lowercase())
}
pub(crate) fn lowercase_at_rule_name(name: &str) -> Cow<'_, str> {
if !tsv_lang::swar::has_ascii_uppercase(name.as_bytes()) || name.contains('\\') {
return Cow::Borrowed(name);
}
Cow::Owned(name.to_ascii_lowercase())
}
pub(crate) fn extract_string_value(
decl_source: &str,
colon_pos: usize,
quote: char,
) -> Option<String> {
let value_part = decl_source[colon_pos + 1..].trim();
if value_part.len() >= 2 {
let raw_content = &value_part[1..value_part.len() - 1];
let formatted = format_string_literal(raw_content, quote);
return Some(formatted);
}
None
}
pub(crate) fn extract_value_with_comments(decl_source: &str, colon_pos: usize) -> String {
normalize_value_with_comments(&decl_source[colon_pos + 1..])
}
pub(crate) fn normalize_value_with_comments(value: &str) -> String {
normalize_value_text(&normalize_css_whitespace(value), ValueReader::Value)
}
#[cfg(test)]
mod tests {
use super::*;
fn has_block_comment(src: &str) -> bool {
src.contains("/*")
}
#[test]
fn test_extract_property_name_borrows_common_case() {
let colon = |s: &str| s.find(':').unwrap();
for src in [
"margin: 10px",
"color:red",
"--custom-prop: var(--x)",
"grid-template-columns: 1fr 2fr",
] {
let got = extract_property_name(src, colon(src), has_block_comment(src));
assert!(
matches!(got, Cow::Borrowed(_)),
"bare property in {src:?} must borrow, got owned {got:?}"
);
}
assert_eq!(
extract_property_name("margin: 10px", colon("margin: 10px"), false),
"margin"
);
assert_eq!(
extract_property_name("color:red", colon("color:red"), false),
"color"
);
assert_eq!(
extract_property_name(
"--custom-prop: var(--x)",
colon("--custom-prop: var(--x)"),
false
),
"--custom-prop"
);
}
#[test]
fn test_extract_property_name_trims_but_still_borrows() {
let src = "color : red";
let got = extract_property_name(src, src.find(':').unwrap(), has_block_comment(src));
assert_eq!(got, "color");
assert!(matches!(got, Cow::Borrowed(_)));
}
#[test]
fn test_extract_property_name_comment_owns() {
let src = "color/* comment */:red";
let got = extract_property_name(src, src.find(':').unwrap(), has_block_comment(src));
assert_eq!(got, "color /* comment */");
assert!(matches!(got, Cow::Owned(_)));
}
#[test]
fn test_extract_property_name_hex_escape_terminator_owns() {
let src = "\\41 : red";
let got = extract_property_name(src, src.find(':').unwrap(), has_block_comment(src));
assert_eq!(got, "\\41 ");
assert!(matches!(got, Cow::Owned(_)));
let src = "color : red";
let got = extract_property_name(src, src.find(':').unwrap(), has_block_comment(src));
assert_eq!(got, "color");
assert!(matches!(got, Cow::Borrowed(_)));
}
#[test]
fn test_lowercase_property_name() {
let low = |s: &str| lowercase_property_name(Cow::Borrowed(s)).into_owned();
assert_eq!(low("COLOR"), "color");
assert_eq!(low("Background-Color"), "background-color");
assert_eq!(low("-WEBKIT-Box-Shadow"), "-webkit-box-shadow");
assert!(matches!(
lowercase_property_name(Cow::Borrowed("color")),
Cow::Borrowed("color")
));
assert_eq!(low("--MyVar"), "--MyVar"); assert_eq!(low("$fontFamily"), "$fontFamily"); assert_eq!(low("#{$Foo}"), "#{$Foo}"); assert_eq!(low("COLOR /* C */"), "COLOR /* C */"); assert_eq!(low("\\43OLOR"), "\\43OLOR"); }
#[test]
fn test_lowercase_at_rule_name() {
let low = |s: &str| lowercase_at_rule_name(s).into_owned();
assert_eq!(low("MEDIA"), "media");
assert_eq!(low("Font-Face"), "font-face");
assert_eq!(low("-WEBKIT-KEYFRAMES"), "-webkit-keyframes");
assert_eq!(low("INCLUDE"), "include"); assert!(matches!(
lowercase_at_rule_name("media"),
Cow::Borrowed("media")
));
assert_eq!(low("\\4D edia"), "\\4D edia");
}
#[test]
fn test_lowercase_media_feature_names() {
let f = |s: &str| lowercase_media_feature_names(s).into_owned();
assert_eq!(f("(MIN-WIDTH: 100px)"), "(min-width: 100px)");
assert_eq!(f("(ORIENTATION: LANDSCAPE)"), "(orientation: LANDSCAPE)");
assert_eq!(f("(HOVER)"), "(hover)"); assert_eq!(f("(WIDTH >= 600px)"), "(width >= 600px)"); assert_eq!(f("(600px <= WIDTH)"), "(600px <= width)"); assert_eq!(
f("SCREEN and (MIN-WIDTH: 1px)"),
"SCREEN and (min-width: 1px)"
);
assert_eq!(f("(--SMALL-VIEWPORT)"), "(--SMALL-VIEWPORT)");
assert_eq!(
f("(MIN-WIDTH: calc(100px + 1px))"),
"(min-width: calc(100px + 1px))"
);
assert_eq!(f("(WIDTH: min(50%, 100px))"), "(width: min(50%, 100px))");
assert_eq!(
f("((MIN-WIDTH: 1px) and (MAX-WIDTH: 2px))"),
"((MIN-WIDTH: 1px) and (MAX-WIDTH: 2px))"
);
assert_eq!(
f("((MIN-WIDTH: calc(1px)) and (b))"),
"((MIN-WIDTH: calc(1px)) and (b))"
);
assert_eq!(f("(NOT (HOVER))"), "(NOT (HOVER))");
assert!(matches!(
lowercase_media_feature_names("(min-width: 100px)"),
Cow::Borrowed(_)
));
assert_eq!(f("(MIN-WIDTH: /* c */ 1px)"), "(min-width: /* c */ 1px)");
assert_eq!(f("CALC(A)"), "CALC(A)");
assert_eq!(
f("CALC(A) and (MIN-WIDTH: 1px)"),
"CALC(A) and (min-width: 1px)"
);
assert_eq!(f("(CALC(1:2): A)"), "(calc(1:2): A)");
assert_eq!(f("(A(B:C))"), "(a(b:C))");
}
#[test]
fn test_normalize_value_text_hex() {
let sup = |s: &str| normalize_value_text(s, ValueReader::Value);
assert_eq!(sup("(background: #FFF)"), "(background: #fff)"); assert_eq!(sup("(a: #ABCD)"), "(a: #abcd)"); assert_eq!(sup("(a: #AABBCC)"), "(a: #aabbcc)"); assert_eq!(sup("(a: #AABBCCDD)"), "(a: #aabbccdd)"); assert_eq!(sup("(a: #1E2)"), "(a: #1e2)"); assert_eq!(sup("(a: #ABCDE)"), "(a: #ABCDE)"); assert_eq!(sup("(a: #ABCDEFA)"), "(a: #ABCDEFA)"); assert_eq!(sup("(a: #GG)"), "(a: #GG)"); assert_eq!(sup("(background: url(#ABC))"), "(background: url(#ABC))");
assert_eq!(sup("selector(#ABC)"), "selector(#ABC)");
assert_eq!(sup("URL(#ABC)"), "URL(#ABC)");
assert_eq!(sup("url(#ABC) #FFF"), "url(#ABC) #fff");
assert_eq!(sup("var(--x, #ABC)"), "var(--x, #abc)");
assert_eq!(sup("(margin: .5px)"), "(margin: 0.5px)");
assert_eq!(
sup("(background: url(sprite1.50.png))"),
"(background: url(sprite1.50.png))"
);
assert_eq!(sup("(width: url(A.5PX))"), "(width: url(A.5PX))");
assert_eq!(sup("url()"), "url()"); assert_eq!(
normalize_value_text("(background: url(\"v2.00.png\"))", ValueReader::Value),
"(background: url('v2.00.png'))"
);
assert_eq!(
normalize_value_text("(a: url(x1.50))", ValueReader::MediaQuery),
"(a: url(x1.50))"
);
assert_eq!(
normalize_value_text("(min-width: #FFF)", ValueReader::MediaQuery),
"(min-width: #FFF)"
);
assert_eq!(
normalize_value_text("(margin: .5px)", ValueReader::MediaQuery),
"(margin: 0.5px)"
);
}
#[test]
fn an_escaped_paren_does_not_close_a_url_token() {
assert_eq!(
normalize_value_text(r"(a: url(x\)1.50))", ValueReader::Value),
r"(a: url(x\)1.50))"
);
assert_eq!(
normalize_value_text(r"(a: url(x\)y#FFF))", ValueReader::Value),
r"(a: url(x\)y#FFF))"
);
assert_eq!(
normalize_value_text(r"(a: url(x\(1.50))", ValueReader::Value),
r"(a: url(x\(1.50))"
);
assert_eq!(
normalize_value_text(r"(a: url(x\29 1.50))", ValueReader::Value),
r"(a: url(x\29 1.50))"
);
assert_eq!(
normalize_value_text(r"(a: url(x\)1.50)", ValueReader::Value),
r"(a: url(x\)1.50)"
);
assert_eq!(
normalize_value_text(r"(a: url(x1.50)\)", ValueReader::Value),
r"(a: url(x1.50)\)"
);
assert_eq!(
normalize_value_text("(a: url(x1.50))", ValueReader::Value),
"(a: url(x1.50))"
);
}
#[test]
fn an_escaped_paren_is_not_a_media_sub_condition() {
assert_eq!(
lowercase_media_feature_names(r"(MIN-WIDTH: a\(b\)c)"),
r"(min-width: a\(b\)c)"
);
assert_eq!(
lowercase_media_feature_names(r"(MIN-WIDTH: a\(b)"),
r"(min-width: a\(b)"
);
assert_eq!(
lowercase_media_feature_names(r"(MIN-WIDTH: a\)b)"),
r"(min-width: a\)b)"
);
assert_eq!(
lowercase_media_feature_names(r"(MIN-WIDTH: a\(b) and (MAX-WIDTH: 1px)"),
r"(min-width: a\(b) and (max-width: 1px)"
);
assert_eq!(
lowercase_media_feature_names("(MIN-WIDTH: calc(1px))"),
"(min-width: calc(1px))"
);
assert_eq!(
lowercase_media_feature_names("((MIN-WIDTH: 1px))"),
"((MIN-WIDTH: 1px))"
);
assert_eq!(
lowercase_media_feature_names(r"a\(b and (MIN-WIDTH: 1px)"),
r"a\(b and (min-width: 1px)"
);
}
#[test]
fn an_escaped_colon_does_not_end_a_feature_name() {
assert_eq!(
lowercase_media_feature_names(r"(A\:B: 1px)"),
r"(a\:b: 1px)"
);
assert_eq!(
lowercase_media_feature_names(r"(--A\:B: 1px)"),
r"(--A\:B: 1px)"
);
assert_eq!(
lowercase_media_feature_names(r"(--A\42 C: 1px)"),
r"(--A\42 C: 1px)"
);
assert_eq!(
lowercase_media_feature_names(r"(--MIN\-WIDTH: 1px)"),
r"(--MIN\-WIDTH: 1px)"
);
assert_eq!(lowercase_media_feature_names(r"(--A\42 C)"), r"(--A\42 C)");
assert_eq!(
lowercase_media_feature_names(r"(MIN\-WIDTH: 1px)"),
r"(min\-width: 1px)"
);
assert_eq!(
lowercase_media_feature_names(r"(A\4B C: 1px)"),
r"(a\4b c: 1px)"
);
assert_eq!(
lowercase_media_feature_names("(A\\\nB: 1px)"),
"(a\\\nb: 1px)"
);
assert_eq!(lowercase_media_feature_names(r"(A: 1px)\"), r"(a: 1px)\");
assert_eq!(
lowercase_media_feature_names(r"(A: LAND\53 CAPE)"),
r"(a: LAND\53 CAPE)"
);
}
#[test]
fn case_sensitivity_is_a_property_of_the_whole_feature_name() {
assert_eq!(
lowercase_media_feature_names(r"(--A B: 1px)"),
r"(--A B: 1px)"
);
assert_eq!(
lowercase_media_feature_names(r"(--A\Z B: 1px)"),
r"(--A\Z B: 1px)"
);
assert_eq!(
lowercase_media_feature_names("(--A 1PX: 1px)"),
"(--A 1PX: 1px)"
);
assert_eq!(
lowercase_media_feature_names("( --A B : 1px)"),
"( --A B : 1px)"
);
assert_eq!(lowercase_media_feature_names("(A$B: 1px)"), "(A$B: 1px)");
assert_eq!(lowercase_media_feature_names("($A: 1px)"), "($A: 1px)");
assert_eq!(lowercase_media_feature_names("(A@B: 1px)"), "(A@B: 1px)");
assert_eq!(lowercase_media_feature_names("(A#B: 1px)"), "(A#B: 1px)");
assert_eq!(lowercase_media_feature_names("(%A: 1px)"), "(%A: 1px)");
assert_eq!(lowercase_media_feature_names("(A%B: 1px)"), "(a%b: 1px)");
assert_eq!(
lowercase_media_feature_names("(MIN-WIDTH: calc(1px))"),
"(min-width: calc(1px))"
);
assert_eq!(
lowercase_media_feature_names("(MIN-WIDTH: url(x:y))"),
"(min-width: url(x:y))"
);
assert_eq!(
lowercase_media_feature_names("(/* : */ MIN-WIDTH: 1px)"),
"(/* : */ min-width: 1px)"
);
assert_eq!(
lowercase_media_feature_names(r"(MIN-WIDTH: ':')"),
r"(min-width: ':')"
);
}
#[test]
fn a_number_abutting_an_identifier_is_copied_verbatim() {
assert_eq!(
normalize_value_text("(a: x1.50)", ValueReader::Value),
"(a: x1.50)"
);
assert_eq!(
normalize_value_text("(a: x.50PX)", ValueReader::Value),
"(a: x.50PX)"
);
assert_eq!(
normalize_value_text("(a: --x.50)", ValueReader::Value),
"(a: --x.50)"
);
assert_eq!(
normalize_value_text("(a: x .50)", ValueReader::Value),
"(a: x 0.5)"
);
assert_eq!(
normalize_value_text("(a: .50px)", ValueReader::Value),
"(a: 0.5px)"
);
assert_eq!(
normalize_value_text("(a: x+1.50)", ValueReader::Value),
"(a: x+1.5)"
);
assert_eq!(
normalize_value_text("(a: x-1.50)", ValueReader::Value),
"(a: x-1.50)"
);
assert_eq!(
normalize_value_text("(a: $.50)", ValueReader::MediaQuery),
"(a: $0.5)"
);
assert_eq!(
normalize_value_text("(a: @.50)", ValueReader::MediaQuery),
"(a: @0.5)"
);
assert_eq!(
normalize_value_text("(a: $x.50)", ValueReader::MediaQuery),
"(a: $x.50)"
);
assert_eq!(
normalize_value_text(r"(a: \41 2.50px)", ValueReader::Value),
r"(a: \41 2.5px)"
);
assert_eq!(
normalize_value_text(r"(a: x\41 .50)", ValueReader::Value),
r"(a: x\41 0.5)"
);
assert_eq!(
normalize_value_text("(a: url(x).50)", ValueReader::Value),
"(a: url(x)0.5)"
);
}
#[test]
fn the_two_value_readers_answer_a_number_differently() {
let value = |s: &str| normalize_value_text(s, ValueReader::Value);
let media = |s: &str| normalize_value_text(s, ValueReader::MediaQuery);
assert_eq!(value("(a: 1.50abc)"), "(a: 1.5abc)");
assert_eq!(media("(a: 1.50abc)"), "(a: 1.50abc)");
assert_eq!(value("(a: 1.50PX)"), "(a: 1.5px)");
assert_eq!(media("(a: 1.50PX)"), "(a: 1.5px)");
assert_eq!(value("(a: 2N)"), "(a: 2N)");
assert_eq!(media("(a: 2N)"), "(a: 2n)");
assert_eq!(value("(a: 2N + 1)"), "(a: 2N + 1)");
assert_eq!(media("(a: 2N + 1)"), "(a: 2n + 1)");
assert_eq!(value("(a: url(1.50))"), "(a: url(1.50))");
assert_eq!(media("(a: url(1.50))"), "(a: url(1.5))");
assert_eq!(media("(a: url(1.50px) b)"), "(a: url(1.5px) b)");
assert_eq!(media("(url(1.50): a)"), "(url(1.50): a)");
assert_eq!(media("(url(1.50))"), "(url(1.50))");
assert_eq!(media("a url(1.50)"), "a url(1.50)");
assert_eq!(media("(a: b) url(1.50)"), "(a: b) url(1.50)");
assert_eq!(value("(a: 1a.50)"), "(a: 1a.50)");
assert_eq!(media("(a: 1a.50)"), "(a: 1a0.5)");
assert_eq!(value("(a: 1.5.50)"), "(a: 1.5.50)");
assert_eq!(media("(a: 1.5.50)"), "(a: 1.50.5)");
assert_eq!(value("(a: #1.50)"), "(a: #1.50)");
assert_eq!(media("(a: #1.50)"), "(a: #1.5)");
assert_eq!(value("(a: #1.50px)"), "(a: #1.50px)");
assert_eq!(media("(a: #1.50px)"), "(a: #1.5px)");
assert_eq!(value("(a: #.50)"), "(a: #0.5)");
assert_eq!(media("(a: #.50)"), "(a: #0.5)");
assert_eq!(value("(a: #abc1.50)"), "(a: #abc1.50)");
assert_eq!(media("(a: #abc1.50)"), "(a: #abc1.50)");
assert_eq!(value("(a: #FFF)"), "(a: #fff)");
assert_eq!(media("(a: #FFF)"), "(a: #FFF)");
assert_eq!(value("(a: $1.50)"), "(a: $1.50)");
assert_eq!(media("(a: $1.50)"), "(a: $1.5)");
assert_eq!(media("(a: x$1.50)"), "(a: x$1.5)");
assert_eq!(value("(a: $a1.50)"), "(a: $a1.50)");
assert_eq!(media("(a: $a1.50)"), "(a: $a1.50)");
assert_eq!(media("(a: $$a.50)"), "(a: $$a.50)");
assert_eq!(media("(a: $-.50)"), "(a: $-0.5)");
}
#[test]
fn the_media_readers_number_pass_skips_feature_name_position() {
let media = |s: &str| normalize_value_text(s, ValueReader::MediaQuery);
assert_eq!(media("(1.50: 2.50)"), "(1.50: 2.5)");
assert_eq!(media("(0.50)"), "(0.50)");
assert_eq!(media("(width >= 1.50)"), "(width >= 1.50)");
assert_eq!(media("(1.50 < width < 2.50)"), "(1.50 < width < 2.50)");
assert_eq!(media("(calc(1.50): a)"), "(calc(1.50): a)");
assert_eq!(media("(calc(1.50))"), "(calc(1.50))");
assert_eq!(media("(calc(1:1.50): a)"), "(calc(1:1.5): a)");
assert_eq!(media("(not (a: 1.50))"), "(not (a: 1.5))");
assert_eq!(
media("((1.50: a) and (b: 1.50))"),
"((1.50: a) and (b: 1.5))"
);
assert_eq!(media("((1.50))"), "((1.50))");
assert_eq!(media("1.50"), "1.5");
assert_eq!(media("1.50, (a: 1.50)"), "1.5, (a: 1.5)");
assert_eq!(media("not (1.50: a)"), "not (1.50: a)");
assert_eq!(media("calc(1.50)"), "calc(1.5)");
assert_eq!(
media("calc(1.50) and (calc(1.50): a)"),
"calc(1.5) and (calc(1.50): a)"
);
assert_eq!(media("(a: 1.50) and (2.50: b)"), "(a: 1.5) and (2.50: b)");
assert_eq!(media("(1.50: a), (b: 2.50)"), "(1.50: a), (b: 2.5)");
assert_eq!(media("(1.50 /* c */: a)"), "(1.50 /* c */: a)");
assert_eq!(media("(/* : */ 1.50: a)"), "(/* : */ 1.50: a)");
assert_eq!(media(r"(a\:b: 1.50)"), r"(a\:b: 1.5)");
assert_eq!(
normalize_value_text("(1.50: 2.50)", ValueReader::Value),
"(1.5: 2.5)"
);
}
#[test]
fn the_hex_fold_reads_the_whole_hash_word() {
let value = |s: &str| normalize_value_text(s, ValueReader::Value);
assert_eq!(value("(a: #FFF.5)"), "(a: #FFF.5)");
assert_eq!(value("(a: #FFF/5)"), "(a: #FFF/5)");
assert_eq!(value("(a: #FFFé)"), "(a: #FFFé)");
assert_eq!(value("(a: #FFF-x)"), "(a: #FFF-x)");
assert_eq!(value("(a: #FFF_x)"), "(a: #FFF_x)");
assert_eq!(value("(a: #ABCD.5)"), "(a: #ABCD.5)");
assert_eq!(value("(a: #FFF 5)"), "(a: #fff 5)");
assert_eq!(value("(a: #FFF)"), "(a: #fff)");
assert_eq!(value("(a: #FFF9)"), "(a: #fff9)");
assert_eq!(value("(a: #.50)"), "(a: #0.5)");
assert_eq!(value("(a: #-.50)"), "(a: #-.50)");
}
}