lightningcss icon indicating copy to clipboard operation
lightningcss copied to clipboard

Transform as an html attribute not parsed correctly

Open noahbald opened this issue 1 year ago • 3 comments

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);

noahbald avatar Oct 14 '24 10:10 noahbald

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.

devongovett avatar Oct 15 '24 20:10 devongovett

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?

noahbald avatar Oct 16 '24 07:10 noahbald

https://github.com/noahbald/oxvg/blob/9cbed0f2d3d70c1b3847f29cf7cc8e12b6ddc1cc/crates/oxvg_ast/src/style.rs#L53-L1431 I've implemented PresentationAttr and SVGTransform in my project which are analogous to Property and Transform.

Would it be worth porting this from my project to lightningcss?

noahbald avatar Jan 02 '25 05:01 noahbald