parcel-bundler / lightningcss

An extremely fast CSS parser, transformer, bundler, and minifier written in Rust.
https://lightningcss.dev
Mozilla Public License 2.0
6.55k stars 190 forks source link

Transform as an html attribute not parsed correctly #834

Open noahbald opened 1 month ago

noahbald commented 1 month ago

Because transform="..." tends to be unit-less and space-separated they aren't parsed correctly by lightning-css despite being a presentation attribute. For example, the following is a valid transform in HTML, but not CSS

/* eg <g transform="translate(0 10)"></g> */
translate(0 10)

While the following is valid in both CSS and HTML

/* eg <g transform="translate(0, 10px)></g> */
/* eg transform: translate(0, 10px); */
translate(0, 10px);

Yet, they are functionally equivalent.

Since in a HTML context the expected syntax is slightly difference, it might be nice to specify in the parser options whether a property being parsed is a presentation attribute

let options = ParserOptions { flags: ParserFlags::HTML, ..ParserOptions::default() }
let transform = properties::Property::parse_string(PropertyId::Transform(VendorPrefix::None), "translate(0 10)", options);
devongovett commented 1 month ago

Yeah the syntax for SVG transform attributes is completely different from CSS. For example rotate takes additional arguments for the center point instead of using transform-origin. I'd probably recommend using a different parser and value type for the presentation attributes vs CSS.

noahbald commented 1 month ago

Yep, absolutely -- SVG transform seems to be a kind of superset of CSS transforms. Thought it'd be worth making this issue for awareness anyway. For what it's worth, I do have a workaround using some (yucky) regex.

use lazy_static::lazy_static;

fn svg_transform_to_css_transform(css_string: String) -> Cow<'_, str> {
    // transform `rotate(r, x, y)` -> `matrix(a, b, c, d, e, f)`
    // see https://github.com/svg/svgo/blob/a8472bc45fe1d92d5f848a08cf4d5c8f4a531ad9/plugins/_transforms.js#L511
    let v = ROTATE_LONG.replace_all(&value, |caps: &regex::Captures| {
        let original = format!("rotate({} {} {})", &caps["r"], &caps["x"], &caps["y"]);
        let Ok(deg) = caps["r"].parse::<f64>() else {
            log::debug!("r failed: {}", &caps["r"]);
            return original;
        };
        let Ok(x) = caps["x"].parse::<f64>() else {
            log::debug!("x failed: {}", &caps["x"]);
            return original;
        };
        let Ok(y) = caps["y"].parse::<f64>() else {
            log::debug!("y failed: {}", &caps["y"]);
            return original;
        };
        let rad = deg.to_radians();
        let cos = rad.cos();
        let sin = rad.sin();
        format!(
            "matrix({cos} {sin} {} {cos} {} {})",
            -sin,
            (1.0 - cos) * x + sin * y,
            (1.0 - cos) * y - sin * x
        )
    });

    // transform `f(a b ...)` -> `f(a, b, ...)`
    let v = LIST_SEP_SPACE.replace_all(&v, "$a, ");

    // transform `rotate(r)` -> `rotate(rdeg)`
    value = ROTATE
        .replace_all(&v, |caps: &regex::Captures| {
            format!("{}({}deg", &caps["f"], &caps["v"])
        })
}

lazy_static! {
    static ref LIST_SEP_SPACE: regex::Regex = regex::Regex::new(r"(?<a>\d)\s+").unwrap();
    static ref ROTATE: regex::Regex =
        regex::Regex::new(r"(?<f>rotate|skewX|skewY)\((?<v>\s*[^\s\),]+)").unwrap();
    static ref ROTATE_LONG: regex::Regex = regex::Regex::new(
        r"rotate\((?<r>[\d\.e-]+)[^\d\)]+?(?<x>[\d\.e-]+)[^\d\)]+?(?<y>[\d\.e-]+)\)"
    )
    .unwrap();
}

In terms of lightningcss, having a separate parser makes sense to me. Would a separate parser mean having something similar to Property but for HTML presentation attributes instead of CSS properties?