omena-transform-passes 0.1.14

Transform pass registry and DAG planner for Omena CSS
Documentation
use omena_parser::LexedToken;

use crate::helpers::{
    declarations::{SimpleDeclarationSlice, declaration_ranges_are_adjacent},
    values::split_top_level_whitespace_value_components,
};

pub(crate) fn collect_background_position_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) =
            background_position_axis_replacement_for_declarations(tokens, pair)
        {
            ranges.push(replacement);
        }
    }
    ranges
}

fn background_position_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_value, y_value) = match (first.property.as_str(), second.property.as_str()) {
        ("background-position-x", "background-position-y") => {
            (first.value.as_str(), second.value.as_str())
        }
        ("background-position-y", "background-position-x") => {
            (second.value.as_str(), first.value.as_str())
        }
        _ => return None,
    };
    let x_component =
        background_position_axis_component(x_value, first.important, PositionAxis::Horizontal)?;
    let y_component =
        background_position_axis_component(y_value, second.important, PositionAxis::Vertical)?;
    let shorthand_value = compressed_background_position_axis_value(&x_component, &y_component);
    let important = if first.important { "!important" } else { "" };

    Some((
        first.start,
        second.end,
        format!("background-position: {shorthand_value}{important};"),
    ))
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PositionAxis {
    Horizontal,
    Vertical,
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct BackgroundPositionAxisComponent {
    value: String,
    keyword: Option<&'static str>,
}

fn background_position_axis_component(
    value: &str,
    important: bool,
    axis: PositionAxis,
) -> Option<BackgroundPositionAxisComponent> {
    let component = single_component_value_without_important(value, important)?;
    if component.contains(',') || is_css_wide_keyword(&component) {
        return None;
    }
    let lower = component.to_ascii_lowercase();
    match (axis, lower.as_str()) {
        (PositionAxis::Horizontal, "left") => Some(background_position_keyword("0", "left")),
        (PositionAxis::Horizontal, "center") => Some(background_position_keyword("50%", "center")),
        (PositionAxis::Horizontal, "right") => Some(background_position_keyword("100%", "right")),
        (PositionAxis::Horizontal, "top" | "bottom") => None,
        (PositionAxis::Vertical, "top") => Some(background_position_keyword("0", "top")),
        (PositionAxis::Vertical, "center") => Some(background_position_keyword("50%", "center")),
        (PositionAxis::Vertical, "bottom") => Some(background_position_keyword("100%", "bottom")),
        (PositionAxis::Vertical, "left" | "right") => None,
        _ => Some(BackgroundPositionAxisComponent {
            value: component,
            keyword: None,
        }),
    }
}

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())
}

fn background_position_keyword(
    value: &'static str,
    keyword: &'static str,
) -> BackgroundPositionAxisComponent {
    BackgroundPositionAxisComponent {
        value: value.to_string(),
        keyword: Some(keyword),
    }
}

fn compressed_background_position_axis_value(
    x: &BackgroundPositionAxisComponent,
    y: &BackgroundPositionAxisComponent,
) -> String {
    if x.value == "50%" && y.value == "50%" {
        return "50%".to_string();
    }
    if y.value == "50%" {
        return x.value.clone();
    }
    if x.keyword == Some("center") {
        if y.keyword == Some("top") {
            return "top".to_string();
        }
        if y.keyword == Some("bottom") {
            return "bottom".to_string();
        }
    }
    format!("{} {}", x.value, y.value)
}

fn is_css_wide_keyword(value: &str) -> bool {
    matches!(
        value.to_ascii_lowercase().as_str(),
        "inherit" | "initial" | "revert" | "revert-layer" | "unset"
    )
}