use omena_cascade::{
BoxLonghandInputV0, ShorthandCombinationProofV0, prove_box_shorthand_combination,
};
use omena_parser::{LexedToken, StyleDialect, lex};
use omena_syntax::SyntaxKind;
use crate::{
domains::{
number::{compress_number_prefix, format_css_number, numeric_prefix_end},
shorthand_font::{
compress_existing_font_shorthand_value, font_shorthand_replacement_for_declarations,
},
shorthand_line::border_side_shorthand_replacement_for_declarations,
shorthand_line::line_shorthand_replacement_for_declarations,
shorthand_line::logical_line_axis_shorthand_replacement_for_declarations,
shorthand_line::logical_line_axis_shorthand_replacement_for_longhand_declarations,
shorthand_list::{
compress_list_style_value, list_style_shorthand_replacement_for_declarations,
},
shorthand_logical::collect_logical_axis_replacements,
shorthand_motion::{
animation_shorthand_replacement_for_declarations, compress_animation_value,
compress_transition_value, transition_shorthand_replacement_for_declarations,
},
shorthand_position::collect_background_position_axis_replacements,
shorthand_text::{
collect_text_emphasis_replacements, compress_text_decoration_value,
compress_text_emphasis_position_value,
text_decoration_shorthand_replacement_for_declarations,
},
},
helpers::{
ascii::normalize_ascii_whitespace,
declarations::{
SimpleDeclarationSlice, collect_simple_declarations_in_block,
declaration_ranges_are_adjacent, format_replacement_declaration_like_source,
},
tokens::matching_right_brace_index,
values::split_top_level_value_arguments,
values::split_top_level_whitespace_value_components,
},
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct BoxShorthandProofCandidateV0 {
pub(crate) source_span_start: usize,
pub(crate) source_span_end: usize,
pub(crate) shorthand_property: &'static str,
pub(crate) longhands: Vec<BoxLonghandInputV0>,
pub(crate) proof: ShorthandCombinationProofV0,
}
pub(crate) fn combine_css_shorthands_with_lexer(
source: &str,
dialect: StyleDialect,
) -> (String, usize) {
let lexed = lex(source, dialect);
let tokens = lexed.tokens();
let mut ranges = collect_shorthand_replacement_ranges(source, tokens);
if ranges.is_empty() {
return (source.to_string(), 0);
}
ranges.sort_by_key(|(start, _, _)| *start);
let mut output = String::with_capacity(source.len());
let mut cursor = 0;
for (start, end, replacement) in &ranges {
if *start > cursor {
output.push_str(&source[cursor..*start]);
}
output.push_str(replacement);
cursor = *end;
}
if cursor < source.len() {
output.push_str(&source[cursor..]);
}
(output, ranges.len())
}
pub(crate) fn collect_box_shorthand_proof_candidates_with_lexer(
source: &str,
dialect: StyleDialect,
) -> Vec<BoxShorthandProofCandidateV0> {
let lexed = lex(source, dialect);
let tokens = lexed.tokens();
let mut candidates = Vec::new();
let mut index = 0;
while index < tokens.len() {
if tokens[index].kind == SyntaxKind::LeftBrace
&& let Some(close_index) = matching_right_brace_index(tokens, index)
{
candidates.extend(collect_box_shorthand_proof_candidates_in_block(
tokens,
index,
close_index,
));
index += 1;
continue;
}
index += 1;
}
candidates
}
fn collect_shorthand_replacement_ranges(
source: &str,
tokens: &[LexedToken],
) -> Vec<(usize, usize, String)> {
let mut ranges = Vec::new();
let mut index = 0;
while index < tokens.len() {
if tokens[index].kind == SyntaxKind::LeftBrace
&& let Some(close_index) = matching_right_brace_index(tokens, index)
{
ranges.extend(collect_shorthand_replacements_in_block(
source,
tokens,
index,
close_index,
));
index += 1;
continue;
}
index += 1;
}
ranges
}
fn collect_shorthand_replacements_in_block(
source: &str,
tokens: &[LexedToken],
block_start: usize,
block_end: usize,
) -> Vec<(usize, usize, String)> {
let declarations = collect_simple_declarations_in_block(tokens, block_start, block_end);
let mut ranges = Vec::new();
let mut index = 0;
while index + 7 < declarations.len() {
if let Some((start, end, replacement)) = animation_shorthand_replacement_for_declarations(
tokens,
&declarations[index..index + 8],
) {
ranges.push((start, end, replacement));
index += 8;
} else {
index += 1;
}
}
let mut index = 0;
while index + 6 < declarations.len() {
if let Some((start, end, replacement)) =
font_shorthand_replacement_for_declarations(tokens, &declarations[index..index + 7])
{
ranges.push((start, end, replacement));
index += 7;
} else {
index += 1;
}
}
let mut index = 0;
while index + 5 < declarations.len() {
if let Some((start, end, replacement)) =
logical_line_axis_shorthand_replacement_for_longhand_declarations(
tokens,
&declarations[index..index + 6],
)
{
ranges.push((start, end, replacement));
index += 6;
} else {
index += 1;
}
}
let mut index = 0;
while index + 4 < declarations.len() {
if let Some((start, end, replacement)) = border_image_shorthand_replacement_for_declarations(
tokens,
&declarations[index..index + 5],
) {
ranges.push((start, end, replacement));
index += 5;
} else {
index += 1;
}
}
let mut index = 0;
while index + 3 < declarations.len() {
if let Some((start, end, replacement)) = border_side_shorthand_replacement_for_declarations(
tokens,
&declarations[index..index + 4],
)
.or_else(|| {
box_shorthand_replacement_for_declarations(tokens, &declarations[index..index + 4])
})
.or_else(|| {
border_radius_shorthand_replacement_for_declarations(
tokens,
&declarations[index..index + 4],
)
})
.or_else(|| {
inset_shorthand_replacement_for_declarations(tokens, &declarations[index..index + 4])
})
.or_else(|| {
text_decoration_shorthand_replacement_for_declarations(
tokens,
&declarations[index..index + 4],
)
})
.or_else(|| {
transition_shorthand_replacement_for_declarations(
tokens,
&declarations[index..index + 4],
)
}) {
ranges.push((start, end, replacement));
index += 4;
} else {
index += 1;
}
}
let mut index = 0;
while index + 2 < declarations.len() {
if let Some((start, end, replacement)) = list_style_shorthand_replacement_for_declarations(
tokens,
&declarations[index..index + 3],
)
.or_else(|| {
line_shorthand_replacement_for_declarations(tokens, &declarations[index..index + 3])
}) {
if !replacement_range_overlaps_existing(&ranges, start, end) {
ranges.push((start, end, replacement));
}
index += 3;
} else {
index += 1;
}
}
ranges.extend(collect_overflow_axis_replacements(tokens, &declarations));
ranges.extend(collect_place_axis_replacements(tokens, &declarations));
ranges.extend(collect_gap_axis_replacements(tokens, &declarations));
ranges.extend(collect_text_emphasis_replacements(tokens, &declarations));
ranges.extend(collect_background_position_axis_replacements(
tokens,
&declarations,
));
for (start, end, replacement) in
collect_background_component_replacements(tokens, &declarations)
{
if !replacement_range_overlaps_existing(&ranges, start, end) {
ranges.push((start, end, replacement));
}
}
for declaration in &declarations {
if let Some((start, end, replacement)) =
shorthand_value_replacement_for_declaration(source, declaration)
&& !replacement_range_overlaps_existing(&ranges, start, end)
{
ranges.push((start, end, replacement));
}
}
ranges.extend(collect_flex_flow_replacements(tokens, &declarations));
ranges.extend(collect_flex_longhand_replacements(tokens, &declarations));
for (start, end, replacement) in collect_logical_line_axis_replacements(tokens, &declarations) {
if !replacement_range_overlaps_existing(&ranges, start, end) {
ranges.push((start, end, replacement));
}
}
ranges.extend(collect_logical_axis_replacements(tokens, &declarations));
for (start, end, replacement) in collect_overridden_flex_longhand_replacements(&declarations) {
if !replacement_range_overlaps_existing(&ranges, start, end) {
ranges.push((start, end, replacement));
}
}
ranges
}
fn replacement_range_overlaps_existing(
ranges: &[(usize, usize, String)],
start: usize,
end: usize,
) -> bool {
ranges
.iter()
.any(|(existing_start, existing_end, _)| start < *existing_end && *existing_start < end)
}
fn box_shorthand_replacement_for_declarations(
tokens: &[LexedToken],
declarations: &[SimpleDeclarationSlice],
) -> Option<(usize, usize, String)> {
let shorthand_property = match declarations.first()?.property.as_str() {
"margin-top" => "margin",
"padding-top" => "padding",
"border-top-color" => "border-color",
"border-top-style" => "border-style",
"border-top-width" => "border-width",
"scroll-margin-top" => "scroll-margin",
"scroll-padding-top" => "scroll-padding",
_ => return None,
};
if !declaration_ranges_are_adjacent(tokens, declarations) {
return None;
}
let proof_inputs = declarations
.iter()
.map(|declaration| BoxLonghandInputV0 {
property: declaration.property.clone(),
value: declaration.value.clone(),
important: declaration.important,
source_order: declaration.source_order,
})
.collect::<Vec<_>>();
let proof = prove_box_shorthand_combination(shorthand_property, &proof_inputs);
if !proof.accepted {
return None;
}
let values = declarations
.iter()
.map(|declaration| declaration.value.as_str())
.collect::<Vec<_>>();
let shorthand_value = compress_box_shorthand_values(&values)?;
let replacement = format!("{shorthand_property}: {shorthand_value};");
Some((
declarations.first()?.start,
declarations.last()?.end,
replacement,
))
}
fn collect_box_shorthand_proof_candidates_in_block(
tokens: &[LexedToken],
block_start: usize,
block_end: usize,
) -> Vec<BoxShorthandProofCandidateV0> {
let declarations = collect_simple_declarations_in_block(tokens, block_start, block_end);
let mut candidates = Vec::new();
for window in declarations.windows(4) {
let Some(shorthand_property) =
box_shorthand_property_for_first_longhand(&window[0].property)
else {
continue;
};
let proof_inputs = window
.iter()
.map(|declaration| BoxLonghandInputV0 {
property: declaration.property.clone(),
value: declaration.value.clone(),
important: declaration.important,
source_order: declaration.source_order,
})
.collect::<Vec<_>>();
let proof = prove_box_shorthand_combination(shorthand_property, &proof_inputs);
candidates.push(BoxShorthandProofCandidateV0 {
source_span_start: window.first().map_or(0, |declaration| declaration.start),
source_span_end: window.last().map_or(0, |declaration| declaration.end),
shorthand_property,
longhands: proof_inputs,
proof,
});
}
candidates
}
fn box_shorthand_property_for_first_longhand(property: &str) -> Option<&'static str> {
match property {
"margin-top" => Some("margin"),
"padding-top" => Some("padding"),
"border-top-color" => Some("border-color"),
"border-top-style" => Some("border-style"),
"border-top-width" => Some("border-width"),
"scroll-margin-top" => Some("scroll-margin"),
"scroll-padding-top" => Some("scroll-padding"),
_ => None,
}
}
fn shorthand_value_replacement_for_declaration(
source: &str,
declaration: &SimpleDeclarationSlice,
) -> Option<(usize, usize, String)> {
let value = if declaration.important {
declaration_value_without_important(&declaration.value)?
} else {
declaration.value.as_str()
};
let mut replacement_value = if is_box_shorthand_property(&declaration.property) {
compress_box_shorthand_value(value)
} else if is_border_none_shorthand_property(&declaration.property) {
compress_border_none_shorthand_value(value)
} else if is_repeat_shorthand_property(&declaration.property) {
compress_background_repeat_value(value)
} else if declaration.property == "overflow" {
compress_overflow_shorthand_value(value)
} else if is_repeated_two_axis_shorthand_property(&declaration.property) {
compress_repeated_two_axis_value(value)
} else if declaration.property == "border-radius" {
compress_border_radius_value(value)
} else if declaration.property == "flex" {
compress_flex_value(value)
} else if declaration.property == "flex-flow" {
compress_flex_flow_value(value)
} else if is_place_axis_shorthand_property(&declaration.property) {
compress_place_axis_shorthand_value(&declaration.property, value)
} else if declaration.property == "gap" {
compress_gap_value(value, declaration.important)
} else if declaration.property == "inset" {
compress_box_shorthand_value(value)
} else if declaration.property == "list-style" {
compress_list_style_value(value)
} else if declaration.property == "transition" {
compress_transition_value(value)
} else if declaration.property == "animation" {
compress_animation_value(value)
} else if declaration.property == "font" {
compress_existing_font_shorthand_value(value)
} else if declaration.property == "text-decoration" {
compress_text_decoration_value(value, declaration.important)
} else if declaration.property == "text-emphasis-position" {
compress_text_emphasis_position_value(value, declaration.important)
} else {
None
}?;
if declaration.important {
replacement_value.push_str("!important");
}
let replacement =
format_replacement_declaration_like_source(source, declaration, &replacement_value);
Some((declaration.start, declaration.end, replacement))
}
fn declaration_value_without_important(value: &str) -> Option<&str> {
let trimmed = value.trim_end();
let lower = trimmed.to_ascii_lowercase();
if !lower.ends_with("!important") {
return None;
}
let suffix_start = trimmed.len().saturating_sub("!important".len());
Some(trimmed[..suffix_start].trim_end())
}
fn border_radius_shorthand_replacement_for_declarations(
tokens: &[LexedToken],
declarations: &[SimpleDeclarationSlice],
) -> Option<(usize, usize, String)> {
let [top_left, top_right, bottom_right, bottom_left] = declarations else {
return None;
};
if top_left.property != "border-top-left-radius"
|| top_right.property != "border-top-right-radius"
|| bottom_right.property != "border-bottom-right-radius"
|| bottom_left.property != "border-bottom-left-radius"
|| declarations.iter().any(|declaration| declaration.important)
|| !declaration_ranges_are_adjacent(tokens, declarations)
{
return None;
}
let corner_axes = declarations
.iter()
.map(|declaration| border_radius_corner_axes(&declaration.value))
.collect::<Option<Vec<_>>>()?;
let horizontal_values = corner_axes
.iter()
.map(|(horizontal, _)| horizontal.as_str())
.collect::<Vec<_>>();
let vertical_values = corner_axes
.iter()
.map(|(_, vertical)| vertical.as_str())
.collect::<Vec<_>>();
let horizontal = compress_box_shorthand_values(&horizontal_values)?;
let vertical = compress_box_shorthand_values(&vertical_values)?;
let shorthand_value = if horizontal == vertical {
horizontal
} else {
format!("{horizontal}/{vertical}")
};
Some((
top_left.start,
bottom_left.end,
format!("border-radius: {shorthand_value};"),
))
}
fn border_image_shorthand_replacement_for_declarations(
tokens: &[LexedToken],
declarations: &[SimpleDeclarationSlice],
) -> Option<(usize, usize, String)> {
let [source, slice, width, outset, repeat] = declarations else {
return None;
};
if source.property != "border-image-source"
|| slice.property != "border-image-slice"
|| width.property != "border-image-width"
|| outset.property != "border-image-outset"
|| repeat.property != "border-image-repeat"
|| declarations
.iter()
.any(|declaration| declaration.important != source.important)
|| !declaration_ranges_are_adjacent(tokens, declarations)
{
return None;
}
let source_value = normalize_border_image_source_value(&source.value)?;
let slice_value = normalize_border_image_axis_value(&slice.value, true)?;
let width_value = normalize_border_image_axis_value(&width.value, false)?;
let outset_value = normalize_border_image_axis_value(&outset.value, false)?;
let repeat_value = normalize_border_image_repeat_value(&repeat.value)?;
let shorthand_value = compressed_border_image_value(
&source_value,
&slice_value,
&width_value,
&outset_value,
&repeat_value,
)?;
let important = if source.important { "!important" } else { "" };
Some((
source.start,
repeat.end,
format!("border-image: {shorthand_value}{important};"),
))
}
fn collect_background_component_replacements(
tokens: &[LexedToken],
declarations: &[SimpleDeclarationSlice],
) -> Vec<(usize, usize, String)> {
let mut ranges = Vec::new();
for triple in declarations.windows(3) {
if let Some(replacement) = background_component_shorthand_replacement_for_declarations(
tokens,
declarations,
triple,
) {
ranges.push(replacement);
}
}
ranges
}
fn background_component_shorthand_replacement_for_declarations(
tokens: &[LexedToken],
block_declarations: &[SimpleDeclarationSlice],
declarations: &[SimpleDeclarationSlice],
) -> Option<(usize, usize, String)> {
let [image, repeat, color] = declarations else {
return None;
};
if image.property != "background-image"
|| repeat.property != "background-repeat"
|| color.property != "background-color"
|| image.important != repeat.important
|| image.important != color.important
|| !declaration_ranges_are_adjacent(tokens, declarations)
|| block_has_other_background_reset_sensitive_declarations(block_declarations, declarations)
{
return None;
}
let important = image.important;
let image_value = single_background_component_value(&image.value, important)?;
let repeat_value = single_background_component_value(&repeat.value, important)
.and_then(|value| normalize_background_repeat_component_value(&value))?;
let color_value = single_background_component_value(&color.value, important)?;
let important = if important { "!important" } else { "" };
Some((
image.start,
color.end,
format!("background: {image_value} {repeat_value} {color_value}{important};"),
))
}
fn block_has_other_background_reset_sensitive_declarations(
block_declarations: &[SimpleDeclarationSlice],
replacement_declarations: &[SimpleDeclarationSlice],
) -> bool {
block_declarations.iter().any(|declaration| {
background_reset_sensitive_property(&declaration.property)
&& !replacement_declarations
.iter()
.any(|replacement| replacement.start == declaration.start)
})
}
fn background_reset_sensitive_property(property: &str) -> bool {
property == "background" || property.starts_with("background-")
}
fn single_background_component_value(value: &str, important: bool) -> Option<String> {
let value = if important {
declaration_value_without_important(value)?
} else {
value
};
let components = split_top_level_value_arguments(value)?;
let [component] = components.as_slice() else {
return None;
};
Some(normalize_ascii_whitespace(component))
}
fn normalize_background_repeat_component_value(value: &str) -> Option<String> {
if let Some(compressed) = compress_background_repeat_value(value) {
return Some(compressed);
}
let components = split_top_level_whitespace_value_components(value)?;
match components.as_slice() {
[single] if is_background_repeat_single_keyword(&single.to_ascii_lowercase()) => {
Some(single.to_ascii_lowercase())
}
[first, second] => {
let first = first.to_ascii_lowercase();
let second = second.to_ascii_lowercase();
(is_background_repeat_axis_keyword(&first)
&& is_background_repeat_axis_keyword(&second))
.then(|| format!("{first} {second}"))
}
_ => None,
}
}
fn is_background_repeat_single_keyword(value: &str) -> bool {
is_background_repeat_axis_keyword(value) || matches!(value, "repeat-x" | "repeat-y")
}
fn inset_shorthand_replacement_for_declarations(
tokens: &[LexedToken],
declarations: &[SimpleDeclarationSlice],
) -> Option<(usize, usize, String)> {
let [top, right, bottom, left] = declarations else {
return None;
};
if top.property != "top"
|| right.property != "right"
|| bottom.property != "bottom"
|| left.property != "left"
|| declarations.iter().any(|declaration| declaration.important)
|| !declaration_ranges_are_adjacent(tokens, declarations)
{
return None;
}
let values = declarations
.iter()
.map(|declaration| declaration.value.as_str())
.collect::<Vec<_>>();
let shorthand_value = compress_box_shorthand_values(&values)?;
Some((top.start, left.end, format!("inset: {shorthand_value};")))
}
fn collect_logical_line_axis_replacements(
tokens: &[LexedToken],
declarations: &[SimpleDeclarationSlice],
) -> Vec<(usize, usize, String)> {
declarations
.windows(2)
.filter_map(|pair| logical_line_axis_shorthand_replacement_for_declarations(tokens, pair))
.collect()
}
fn collect_overflow_axis_replacements(
tokens: &[LexedToken],
declarations: &[SimpleDeclarationSlice],
) -> Vec<(usize, usize, String)> {
let mut ranges = Vec::new();
for pair in declarations.windows(2) {
if let Some(replacement) = overflow_axis_replacement_for_declarations(tokens, pair) {
ranges.push(replacement);
}
}
ranges
}
fn overflow_axis_replacement_for_declarations(
tokens: &[LexedToken],
declarations: &[SimpleDeclarationSlice],
) -> Option<(usize, usize, String)> {
let [first, second] = declarations else {
return None;
};
if first.important || second.important || !declaration_ranges_are_adjacent(tokens, declarations)
{
return None;
}
let (x, y) = match (first.property.as_str(), second.property.as_str()) {
("overflow-x", "overflow-y") => (first.value.as_str(), second.value.as_str()),
("overflow-y", "overflow-x") => (second.value.as_str(), first.value.as_str()),
_ => return None,
};
let x = normalize_overflow_axis_keyword(x)?;
let y = normalize_overflow_axis_keyword(y)?;
let shorthand_value = compressed_two_axis_shorthand_value(&x, &y);
Some((
first.start,
second.end,
format!("overflow: {shorthand_value};"),
))
}
fn collect_place_axis_replacements(
tokens: &[LexedToken],
declarations: &[SimpleDeclarationSlice],
) -> Vec<(usize, usize, String)> {
let mut ranges = Vec::new();
for pair in declarations.windows(2) {
if let Some(replacement) = place_axis_replacement_for_declarations(tokens, pair) {
ranges.push(replacement);
}
}
ranges
}
fn place_axis_replacement_for_declarations(
tokens: &[LexedToken],
declarations: &[SimpleDeclarationSlice],
) -> Option<(usize, usize, String)> {
let [first, second] = declarations else {
return None;
};
if first.important != second.important || !declaration_ranges_are_adjacent(tokens, declarations)
{
return None;
}
let (shorthand, align_value, justify_value) = place_axis_shorthand_components(first, second)?;
let align_value = normalize_place_axis_value(
shorthand,
PlaceAxisComponentKind::Align,
align_value,
first.important,
)?;
let justify_value = normalize_place_axis_value(
shorthand,
PlaceAxisComponentKind::Justify,
justify_value,
second.important,
)?;
let shorthand_value = compressed_place_axis_value(shorthand, &align_value, &justify_value);
let important = if first.important { "!important" } else { "" };
Some((
first.start,
second.end,
format!("{shorthand}: {shorthand_value}{important};"),
))
}
fn place_axis_shorthand_components<'a>(
first: &'a SimpleDeclarationSlice,
second: &'a SimpleDeclarationSlice,
) -> Option<(&'static str, &'a str, &'a str)> {
match (first.property.as_str(), second.property.as_str()) {
("align-items", "justify-items") => {
Some(("place-items", first.value.as_str(), second.value.as_str()))
}
("justify-items", "align-items") => {
Some(("place-items", second.value.as_str(), first.value.as_str()))
}
("align-content", "justify-content") => {
Some(("place-content", first.value.as_str(), second.value.as_str()))
}
("justify-content", "align-content") => {
Some(("place-content", second.value.as_str(), first.value.as_str()))
}
("align-self", "justify-self") => {
Some(("place-self", first.value.as_str(), second.value.as_str()))
}
("justify-self", "align-self") => {
Some(("place-self", second.value.as_str(), first.value.as_str()))
}
_ => None,
}
}
fn collect_gap_axis_replacements(
tokens: &[LexedToken],
declarations: &[SimpleDeclarationSlice],
) -> Vec<(usize, usize, String)> {
let mut ranges = Vec::new();
for pair in declarations.windows(2) {
if let Some(replacement) = gap_axis_replacement_for_declarations(tokens, pair) {
ranges.push(replacement);
}
}
ranges
}
fn gap_axis_replacement_for_declarations(
tokens: &[LexedToken],
declarations: &[SimpleDeclarationSlice],
) -> Option<(usize, usize, String)> {
let [first, second] = declarations else {
return None;
};
if first.important != second.important || !declaration_ranges_are_adjacent(tokens, declarations)
{
return None;
}
let (row_gap, column_gap) = match (first.property.as_str(), second.property.as_str()) {
("row-gap", "column-gap") => (first.value.as_str(), second.value.as_str()),
("column-gap", "row-gap") => (second.value.as_str(), first.value.as_str()),
_ => return None,
};
let row_gap = single_component_value_without_important(row_gap, first.important)?;
let column_gap = single_component_value_without_important(column_gap, second.important)?;
let shorthand_value = compressed_two_axis_shorthand_value(&row_gap, &column_gap);
let important = if first.important { "!important" } else { "" };
Some((
first.start,
second.end,
format!("gap: {shorthand_value}{important};"),
))
}
fn collect_flex_flow_replacements(
tokens: &[LexedToken],
declarations: &[SimpleDeclarationSlice],
) -> Vec<(usize, usize, String)> {
let mut ranges = Vec::new();
for pair in declarations.windows(2) {
if let Some(replacement) = flex_flow_replacement_for_declarations(tokens, pair) {
ranges.push(replacement);
}
}
ranges
}
fn flex_flow_replacement_for_declarations(
tokens: &[LexedToken],
declarations: &[SimpleDeclarationSlice],
) -> Option<(usize, usize, String)> {
let [first, second] = declarations else {
return None;
};
if first.important != second.important || !declaration_ranges_are_adjacent(tokens, declarations)
{
return None;
}
let (direction, wrap) = match (first.property.as_str(), second.property.as_str()) {
("flex-direction", "flex-wrap") => (first.value.as_str(), second.value.as_str()),
("flex-wrap", "flex-direction") => (second.value.as_str(), first.value.as_str()),
_ => return None,
};
let direction = single_component_value_without_important(direction, first.important)?;
let wrap = single_component_value_without_important(wrap, second.important)?;
let shorthand_value = compressed_flex_flow_components(&direction, &wrap)?;
let important = if first.important { "!important" } else { "" };
Some((
first.start,
second.end,
format!("flex-flow: {shorthand_value}{important};"),
))
}
fn collect_flex_longhand_replacements(
tokens: &[LexedToken],
declarations: &[SimpleDeclarationSlice],
) -> Vec<(usize, usize, String)> {
let mut ranges = Vec::new();
for triple in declarations.windows(3) {
if let Some(replacement) = flex_longhand_replacement_for_declarations(tokens, triple) {
ranges.push(replacement);
}
}
ranges
}
fn flex_longhand_replacement_for_declarations(
tokens: &[LexedToken],
declarations: &[SimpleDeclarationSlice],
) -> Option<(usize, usize, String)> {
let [first, second, third] = declarations else {
return None;
};
if first.important != second.important
|| first.important != third.important
|| !declaration_ranges_are_adjacent(tokens, declarations)
{
return None;
}
let mut grow = None;
let mut shrink = None;
let mut basis = None;
for declaration in declarations {
match declaration.property.as_str() {
"flex-grow" => grow = Some(declaration.value.as_str()),
"flex-shrink" => shrink = Some(declaration.value.as_str()),
"flex-basis" => basis = Some(declaration.value.as_str()),
_ => return None,
}
}
let grow = single_component_value_without_important(grow?, first.important)?;
let shrink = single_component_value_without_important(shrink?, first.important)?;
let basis = single_component_value_without_important(basis?, first.important)?;
let shorthand_value = compress_flex_components(&grow, &shrink, &basis)?;
let important = if first.important { "!important" } else { "" };
Some((
first.start,
third.end,
format!("flex: {shorthand_value}{important};"),
))
}
fn collect_overridden_flex_longhand_replacements(
declarations: &[SimpleDeclarationSlice],
) -> Vec<(usize, usize, String)> {
let mut ranges = Vec::new();
for (index, declaration) in declarations.iter().enumerate() {
if !flex_longhand_is_reset_by_flex_shorthand(&declaration.property) {
continue;
}
let later_flex = declarations[index + 1..]
.iter()
.find(|candidate| candidate.property == "flex");
if later_flex.is_some_and(|candidate| later_declaration_overrides(declaration, candidate)) {
ranges.push((declaration.start, declaration.end, String::new()));
}
}
ranges
}
fn flex_longhand_is_reset_by_flex_shorthand(property: &str) -> bool {
matches!(property, "flex-basis" | "flex-grow" | "flex-shrink")
}
fn later_declaration_overrides(
earlier: &SimpleDeclarationSlice,
later: &SimpleDeclarationSlice,
) -> bool {
!earlier.important || later.important
}
fn single_component_value_without_important(value: &str, important: bool) -> Option<String> {
let mut components = split_top_level_whitespace_value_components(value)?;
if important
&& components.last().is_some_and(|component| {
component.eq_ignore_ascii_case("!important")
|| component.eq_ignore_ascii_case("important")
})
{
components.pop();
}
let [component] = components.as_slice() else {
return None;
};
Some(component.clone())
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum PlaceAxisComponentKind {
Align,
Justify,
}
fn normalize_place_axis_value(
shorthand: &str,
component_kind: PlaceAxisComponentKind,
value: &str,
important: bool,
) -> Option<String> {
let mut components = split_top_level_whitespace_value_components(value)?;
if important
&& components.last().is_some_and(|component| {
component.eq_ignore_ascii_case("!important")
|| component.eq_ignore_ascii_case("important")
})
{
components.pop();
}
let components = components
.iter()
.map(|component| component.to_ascii_lowercase())
.collect::<Vec<_>>();
if let [component] = components.as_slice()
&& is_place_single_component_keyword(component)
{
return Some(component.clone());
}
let [first, second] = components.as_slice() else {
return None;
};
let first = first.as_str();
let second = second.as_str();
if first == "first"
&& second == "baseline"
&& place_axis_allows_multi_token_alignment(shorthand)
{
return Some("baseline".to_string());
}
if first == "last" && second == "baseline" && place_axis_allows_multi_token_alignment(shorthand)
{
return Some("last baseline".to_string());
}
if place_axis_allows_multi_token_alignment(shorthand)
&& is_overflow_position_keyword(first)
&& is_place_self_position_keyword(second)
{
return Some(format!("{first} {second}"));
}
if first == "legacy"
&& shorthand == "place-items"
&& component_kind == PlaceAxisComponentKind::Justify
&& is_legacy_justify_items_position(second)
{
return Some(format!("legacy {second}"));
}
None
}
fn place_axis_allows_multi_token_alignment(shorthand: &str) -> bool {
matches!(shorthand, "place-items" | "place-self")
}
fn is_overflow_position_keyword(value: &str) -> bool {
matches!(value, "safe" | "unsafe")
}
fn is_place_self_position_keyword(value: &str) -> bool {
matches!(
value,
"center"
| "start"
| "end"
| "flex-start"
| "flex-end"
| "self-start"
| "self-end"
| "left"
| "right"
)
}
fn is_legacy_justify_items_position(value: &str) -> bool {
matches!(value, "left" | "right" | "center")
}
fn compressed_place_axis_value(shorthand: &str, align_value: &str, justify_value: &str) -> String {
if align_value == justify_value
&& !(matches!(shorthand, "place-items" | "place-self") && align_value == "stretch")
{
align_value.to_string()
} else {
format!("{align_value} {justify_value}")
}
}
fn is_place_axis_shorthand_property(property: &str) -> bool {
matches!(property, "place-content" | "place-items" | "place-self")
}
fn compress_place_axis_shorthand_value(property: &str, value: &str) -> Option<String> {
let components = split_top_level_whitespace_value_components(value)?;
let [align_value, justify_value] = components.as_slice() else {
return None;
};
let align_value =
normalize_place_axis_value(property, PlaceAxisComponentKind::Align, align_value, false)?;
let justify_value = normalize_place_axis_value(
property,
PlaceAxisComponentKind::Justify,
justify_value,
false,
)?;
let replacement = compressed_place_axis_value(property, &align_value, &justify_value);
(replacement != normalize_ascii_whitespace(value)).then_some(replacement)
}
fn is_place_single_component_keyword(value: &str) -> bool {
matches!(
value,
"auto"
| "normal"
| "stretch"
| "center"
| "start"
| "end"
| "flex-start"
| "flex-end"
| "self-start"
| "self-end"
| "left"
| "right"
| "baseline"
| "space-between"
| "space-around"
| "space-evenly"
)
}
fn compress_gap_value(value: &str, important: bool) -> Option<String> {
let mut components = split_top_level_whitespace_value_components(value)?;
if important
&& components.last().is_some_and(|component| {
component.eq_ignore_ascii_case("!important")
|| component.eq_ignore_ascii_case("important")
})
{
components.pop();
}
let [row_gap, column_gap] = components.as_slice() else {
return None;
};
if row_gap != column_gap {
return None;
}
let replacement = row_gap.clone();
(replacement != normalize_ascii_whitespace(value)).then_some(replacement)
}
fn compressed_two_axis_shorthand_value(first: &str, second: &str) -> String {
if first == second {
first.to_string()
} else {
format!("{first} {second}")
}
}
pub(crate) fn is_box_shorthand_property(property: &str) -> bool {
matches!(
property,
"margin"
| "padding"
| "border-color"
| "border-image-outset"
| "border-image-slice"
| "border-image-width"
| "border-style"
| "border-width"
| "scroll-margin"
| "scroll-padding"
)
}
fn is_border_none_shorthand_property(property: &str) -> bool {
matches!(
property,
"border" | "border-top" | "border-right" | "border-bottom" | "border-left" | "outline"
)
}
fn compress_border_none_shorthand_value(value: &str) -> Option<String> {
let components = split_top_level_whitespace_value_components(value)?;
if components.len() != 3 {
return None;
}
let mut style = None;
let mut saw_default_width = false;
let mut saw_default_color = false;
for component in components {
let normalized = component.to_ascii_lowercase();
match normalized.as_str() {
"none" if style.is_none() => style = Some(normalized),
"medium" if !saw_default_width => saw_default_width = true,
"currentcolor" if !saw_default_color => saw_default_color = true,
_ => return None,
}
}
(saw_default_width && saw_default_color).then_some(style?)
}
pub(crate) fn compress_background_repeat_value(value: &str) -> Option<String> {
let components = split_top_level_whitespace_value_components(value)?;
let [x, y] = components.as_slice() else {
return None;
};
let x = x.to_ascii_lowercase();
let y = y.to_ascii_lowercase();
if !is_background_repeat_axis_keyword(&x) || !is_background_repeat_axis_keyword(&y) {
return None;
}
let replacement = if x == "repeat" && y == "no-repeat" {
"repeat-x".to_string()
} else if x == "no-repeat" && y == "repeat" {
"repeat-y".to_string()
} else if x == y {
x
} else {
return None;
};
(replacement != normalize_ascii_whitespace(value)).then_some(replacement)
}
fn is_repeat_shorthand_property(property: &str) -> bool {
matches!(
property,
"background-repeat" | "mask-repeat" | "-webkit-mask-repeat"
)
}
fn is_background_repeat_axis_keyword(value: &str) -> bool {
matches!(value, "repeat" | "no-repeat" | "space" | "round")
}
fn is_repeated_two_axis_shorthand_property(property: &str) -> bool {
matches!(
property,
"border-block-color"
| "border-block-style"
| "border-block-width"
| "border-inline-color"
| "border-inline-style"
| "border-inline-width"
| "border-spacing"
| "inset-block"
| "inset-inline"
| "margin-block"
| "margin-inline"
| "padding-block"
| "padding-inline"
| "scroll-margin-block"
| "scroll-margin-inline"
| "scroll-padding-block"
| "scroll-padding-inline"
)
}
fn compress_repeated_two_axis_value(value: &str) -> Option<String> {
let components = split_top_level_whitespace_value_components(value)?;
let [first, second] = components.as_slice() else {
return None;
};
(first == second).then(|| first.clone())
}
fn compress_overflow_shorthand_value(value: &str) -> Option<String> {
let components = split_top_level_whitespace_value_components(value)?;
let [x, y] = components.as_slice() else {
return None;
};
let x = normalize_overflow_axis_keyword(x)?;
let y = normalize_overflow_axis_keyword(y)?;
let compressed = compressed_two_axis_shorthand_value(&x, &y);
(compressed != normalize_ascii_whitespace(value)).then_some(compressed)
}
pub(crate) fn compress_border_radius_value(value: &str) -> Option<String> {
let components = split_top_level_whitespace_value_components(value)?;
let compressed =
if let Some(slash_index) = components.iter().position(|component| component == "/") {
if components[slash_index + 1..]
.iter()
.any(|component| component == "/")
|| slash_index == 0
|| slash_index + 1 == components.len()
{
return None;
}
let horizontal = compress_border_radius_axis_values(&components[..slash_index])?;
let vertical = compress_border_radius_axis_values(&components[slash_index + 1..])?;
if horizontal == vertical {
horizontal
} else {
format!("{horizontal}/{vertical}")
}
} else {
compress_border_radius_axis_values(&components)?
};
(compressed != normalize_ascii_whitespace(value)).then_some(compressed)
}
fn compress_border_radius_axis_values(components: &[String]) -> Option<String> {
if !(1..=4).contains(&components.len())
|| components
.iter()
.any(|component| !is_single_axis_border_radius_value(component))
{
return None;
}
let values = match components {
[value] => [
value.as_str(),
value.as_str(),
value.as_str(),
value.as_str(),
],
[top_left_bottom_right, top_right_bottom_left] => [
top_left_bottom_right.as_str(),
top_right_bottom_left.as_str(),
top_left_bottom_right.as_str(),
top_right_bottom_left.as_str(),
],
[top_left, top_right_bottom_left, bottom_right] => [
top_left.as_str(),
top_right_bottom_left.as_str(),
bottom_right.as_str(),
top_right_bottom_left.as_str(),
],
[top_left, top_right, bottom_right, bottom_left] => [
top_left.as_str(),
top_right.as_str(),
bottom_right.as_str(),
bottom_left.as_str(),
],
_ => return None,
};
compress_box_shorthand_values(&values)
}
pub(crate) fn is_single_axis_border_radius_value(value: &str) -> bool {
split_top_level_whitespace_value_components(value)
.is_some_and(|components| components.len() == 1 && components[0] != "/")
}
fn border_radius_corner_axes(value: &str) -> Option<(String, String)> {
let components = split_top_level_whitespace_value_components(value)?;
match components.as_slice() {
[axis] if axis != "/" => Some((axis.clone(), axis.clone())),
[horizontal, vertical] if horizontal != "/" && vertical != "/" => {
Some((horizontal.clone(), vertical.clone()))
}
_ => None,
}
}
pub(crate) fn compress_flex_value(value: &str) -> Option<String> {
let components = split_top_level_whitespace_value_components(value)?;
let [grow, shrink, basis] = components.as_slice() else {
return None;
};
let compressed = compress_flex_components(grow, shrink, basis)?;
(compressed.len() < normalize_ascii_whitespace(value).len()).then_some(compressed)
}
fn compress_flex_components(grow: &str, shrink: &str, basis: &str) -> Option<String> {
let grow = normalize_flex_number(grow)?;
let shrink = normalize_flex_number(shrink)?;
match normalize_static_flex_basis_component(basis)? {
StaticFlexBasisComponent::Auto if grow == "0" && shrink == "0" => Some("none".to_string()),
StaticFlexBasisComponent::Auto if shrink == "1" => {
if grow == "1" {
Some("auto".to_string())
} else {
Some(format!("{grow} auto"))
}
}
StaticFlexBasisComponent::ZeroPercent => {
if shrink == "1" {
Some(grow)
} else {
Some(format!("{grow} {shrink}"))
}
}
StaticFlexBasisComponent::Basis(basis) if basis == "0" => {
Some(format!("{grow} {shrink} 0"))
}
StaticFlexBasisComponent::Basis(basis) if shrink == "1" => {
if grow == "1" {
Some(basis)
} else {
Some(format!("{grow} {basis}"))
}
}
_ => None,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum StaticFlexBasisComponent {
Auto,
ZeroPercent,
Basis(String),
}
fn normalize_static_flex_basis_component(value: &str) -> Option<StaticFlexBasisComponent> {
let value = normalize_ascii_whitespace(value);
if value.eq_ignore_ascii_case("auto") {
return Some(StaticFlexBasisComponent::Auto);
}
if value.contains('(') {
return None;
}
let split = numeric_prefix_end(&value)?;
if split == 0 {
return None;
}
let number = &value[..split];
let unit = value[split..].to_ascii_lowercase();
let parsed = number.parse::<f64>().ok()?;
if !parsed.is_finite() || parsed < 0.0 {
return None;
}
if unit.is_empty() && parsed != 0.0 {
return None;
}
if !unit.is_empty() && unit != "%" && !unit.chars().all(|ch| ch.is_ascii_alphabetic()) {
return None;
}
let number = compress_number_prefix(&format_css_number(parsed));
if unit == "%" && parsed == 0.0 {
return Some(StaticFlexBasisComponent::ZeroPercent);
}
if parsed == 0.0 {
return Some(StaticFlexBasisComponent::Basis("0".to_string()));
}
Some(StaticFlexBasisComponent::Basis(format!("{number}{unit}")))
}
fn compress_flex_flow_value(value: &str) -> Option<String> {
let components = split_top_level_whitespace_value_components(value)?;
let [first, second] = components.as_slice() else {
return None;
};
let shorthand_value = compressed_flex_flow_unordered_components(first, second)?;
(shorthand_value.len() < normalize_ascii_whitespace(value).len()).then_some(shorthand_value)
}
fn compressed_flex_flow_unordered_components(first: &str, second: &str) -> Option<String> {
let first = first.to_ascii_lowercase();
let second = second.to_ascii_lowercase();
if is_flex_direction_keyword(&first) && is_flex_wrap_keyword(&second) {
return compressed_flex_flow_components(&first, &second);
}
if is_flex_wrap_keyword(&first) && is_flex_direction_keyword(&second) {
return compressed_flex_flow_components(&second, &first);
}
None
}
fn compressed_flex_flow_components(direction: &str, wrap: &str) -> Option<String> {
let direction = direction.to_ascii_lowercase();
let wrap = wrap.to_ascii_lowercase();
if !is_flex_direction_keyword(&direction) || !is_flex_wrap_keyword(&wrap) {
return None;
}
if direction == "row" && wrap == "nowrap" {
Some("row".to_string())
} else if direction == "row" {
Some(wrap)
} else if wrap == "nowrap" {
Some(direction)
} else {
Some(format!("{direction} {wrap}"))
}
}
fn is_flex_direction_keyword(value: &str) -> bool {
matches!(value, "row" | "row-reverse" | "column" | "column-reverse")
}
fn is_flex_wrap_keyword(value: &str) -> bool {
matches!(value, "nowrap" | "wrap" | "wrap-reverse")
}
fn normalize_flex_number(value: &str) -> Option<String> {
let split = numeric_prefix_end(value)?;
if split != value.len() {
return None;
}
let parsed = value.parse::<f64>().ok()?;
if !parsed.is_finite() || parsed < 0.0 {
return None;
}
Some(compress_number_prefix(&format_css_number(parsed)))
}
pub(crate) fn compress_box_shorthand_value(value: &str) -> Option<String> {
let components = split_top_level_whitespace_value_components(value)?;
let [top, right, bottom, left] = match components.as_slice() {
[value] => [value, value, value, value],
[block, inline] => [block, inline, block, inline],
[top, inline, bottom] => [top, inline, bottom, inline],
[top, right, bottom, left] => [top, right, bottom, left],
_ => return None,
};
let values = [top.as_str(), right.as_str(), bottom.as_str(), left.as_str()];
let compressed = compress_box_shorthand_values(&values)?;
(compressed != normalize_ascii_whitespace(value)).then_some(compressed)
}
pub(crate) fn compress_box_shorthand_values(values: &[&str]) -> Option<String> {
let [top, right, bottom, left] = values else {
return None;
};
let parts = if top == right && top == bottom && top == left {
vec![*top]
} else if top == bottom && right == left {
vec![*top, *right]
} else if right == left {
vec![*top, *right, *bottom]
} else {
vec![*top, *right, *bottom, *left]
};
Some(parts.join(" "))
}
fn normalize_border_image_source_value(value: &str) -> Option<String> {
let value = normalize_ascii_whitespace(value);
(!value.is_empty()).then_some(value)
}
fn normalize_border_image_axis_value(value: &str, allow_fill: bool) -> Option<String> {
let components = split_top_level_whitespace_value_components(value)?;
if components.is_empty() {
return None;
}
if !allow_fill
&& components
.iter()
.any(|component| component.eq_ignore_ascii_case("fill"))
{
return None;
}
let components = components
.into_iter()
.map(|component| {
if component.eq_ignore_ascii_case("fill") {
"fill".to_string()
} else {
component
}
})
.collect::<Vec<_>>();
if components.iter().any(|component| component == "fill") {
return Some(components.join(" "));
}
if !(1..=4).contains(&components.len()) {
return None;
}
let normalized = components.join(" ");
Some(compress_box_shorthand_value(&normalized).unwrap_or(normalized))
}
fn normalize_border_image_repeat_value(value: &str) -> Option<String> {
let components = split_top_level_whitespace_value_components(value)?;
if components.is_empty() || components.len() > 2 {
return None;
}
let components = components
.into_iter()
.map(|component| component.to_ascii_lowercase())
.collect::<Vec<_>>();
if components
.iter()
.any(|component| !is_border_image_repeat_keyword(component))
{
return None;
}
Some(compressed_two_axis_shorthand_value(
components.first()?,
components.get(1).unwrap_or(components.first()?),
))
}
fn compressed_border_image_value(
source: &str,
slice: &str,
width: &str,
outset: &str,
repeat: &str,
) -> Option<String> {
let source_is_default = source.eq_ignore_ascii_case("none");
let slice_is_default = slice.eq_ignore_ascii_case("100%");
let width_is_default = width.eq_ignore_ascii_case("1");
let outset_is_default = outset.eq_ignore_ascii_case("0");
let repeat_is_default = repeat.eq_ignore_ascii_case("stretch");
let mut components = Vec::new();
if !source_is_default {
components.push(source.to_string());
}
if !width_is_default || !outset_is_default {
let slice_component = if slice_is_default {
"100%".to_string()
} else {
slice.to_string()
};
let slash_component = if !width_is_default && !outset_is_default {
format!("{slice_component}/{width}/{outset}")
} else if !width_is_default {
format!("{slice_component}/{width}")
} else {
format!("{slice_component}//{outset}")
};
components.push(slash_component);
} else if !slice_is_default {
components.push(slice.to_string());
}
if !repeat_is_default {
components.push(repeat.to_string());
}
(!components.is_empty()).then(|| components.join(" "))
}
fn is_border_image_repeat_keyword(value: &str) -> bool {
matches!(value, "stretch" | "repeat" | "round" | "space")
}
pub(crate) fn is_overflow_axis_keyword(value: &str) -> bool {
matches!(value, "visible" | "hidden" | "clip" | "scroll" | "auto")
}
fn normalize_overflow_axis_keyword(value: &str) -> Option<String> {
let lower = value.to_ascii_lowercase();
is_overflow_axis_keyword(&lower).then_some(lower)
}