diff --git a/Cargo.lock b/Cargo.lock index cf4e300e..b9e9db84 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -121,9 +121,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.1" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "bitvec" @@ -747,7 +747,7 @@ dependencies = [ "assert_cmd", "assert_fs", "atty", - "bitflags 2.4.1", + "bitflags 2.6.0", "browserslist-rs", "clap", "const-str", @@ -762,6 +762,7 @@ dependencies = [ "jemallocator", "lazy_static", "lightningcss-derive", + "oxvg_path", "parcel_selectors", "parcel_sourcemap", "paste", @@ -878,7 +879,7 @@ version = "2.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72e0dc78e0524286630914db66e31bad70160e379705a9ce92e0161ce2389d89" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.6.0", "ctor", "napi-derive", "napi-sys", @@ -974,11 +975,23 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f222829ae9293e33a9f5e9f440c6760a3d450a64affe1846486b140db81c1f4" +[[package]] +name = "oxvg_path" +version = "0.0.1-beta.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e451f3fe3d6a997e83f0557b03b00175a946e41f155712eff9adc4088d3aac" +dependencies = [ + "bitflags 2.6.0", + "ryu", + "schemars", + "serde", +] + [[package]] name = "parcel_selectors" version = "0.28.0" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.6.0", "cssparser", "log", "phf", @@ -1330,7 +1343,7 @@ version = "0.38.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", @@ -1339,9 +1352,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.15" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "same-file" diff --git a/Cargo.toml b/Cargo.toml index 0f97acaf..4dafb59d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,13 +39,14 @@ browserslist = ["browserslist-rs"] bundler = ["dashmap", "sourcemap", "rayon"] cli = ["atty", "clap", "serde_json", "browserslist", "jemallocator"] grid = [] -jsonschema = ["schemars", "serde", "parcel_selectors/jsonschema"] +jsonschema = ["schemars", "serde", "parcel_selectors/jsonschema", "oxvg_path/jsonschema"] nodejs = ["dep:serde"] serde = [ "dep:serde", "smallvec/serde", "cssparser/serde", "parcel_selectors/serde", + "oxvg_path/serde", "into_owned", ] sourcemap = ["parcel_sourcemap"] @@ -73,6 +74,7 @@ pathdiff = "0.2.1" ahash = "0.8.7" paste = "1.0.12" indexmap = "2.2.6" +oxvg_path = "0.0.1-beta.4" # CLI deps atty = { version = "0.2", optional = true } clap = { version = "3.0.6", features = ["derive"], optional = true } diff --git a/src/lib.rs b/src/lib.rs index 53a1811e..12552933 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25010,6 +25010,18 @@ mod tests { ".foo { clip-path: polygon(evenodd, 50% 0%, 100% 50%, 50% 100%, 0% 50%); }", ".foo{clip-path:polygon(evenodd,50% 0%,100% 50%,50% 100%,0% 50%)}", ); + minify_test( + r#".foo { clip-path: path("M 0 0 L 0 10"); }"#, + r#".foo{clip-path:path("M0 0v10")}"#, + ); + minify_test( + r#".foo { clip-path: path(nonzero, "M 0 0 L 10 20"); }"#, + r#".foo{clip-path:path("m0 0 10 20")}"#, + ); + minify_test( + r#".foo { clip-path: path(evenodd, "M 0 0 L 10 20"); }"#, + r#".foo{clip-path:path(evenodd,"m0 0 10 20")}"#, + ); minify_test( ".foo { clip-path: padding-box circle(50px at 0 100px); }", ".foo{clip-path:circle(50px at 0 100px) padding-box}", diff --git a/src/values/shape.rs b/src/values/shape.rs index 9cb5f585..f158670e 100644 --- a/src/values/shape.rs +++ b/src/values/shape.rs @@ -1,5 +1,7 @@ //! CSS shape values for masking and clipping. +use std::fmt::Write; + use super::length::LengthPercentage; use super::position::Position; use super::rect::Rect; @@ -9,8 +11,9 @@ use crate::printer::Printer; use crate::properties::border_radius::BorderRadius; use crate::traits::{Parse, ToCss}; #[cfg(feature = "visitor")] -use crate::visitor::Visit; +use crate::visitor::{Visit, VisitTypes, Visitor}; use cssparser::*; +use oxvg_path; /// A CSS [``](https://www.w3.org/TR/css-shapes-1/#basic-shape-functions) value. #[derive(Debug, Clone, PartialEq)] @@ -31,6 +34,8 @@ pub enum BasicShape { Ellipse(Ellipse), /// A polygon. Polygon(Polygon), + /// A path. + Path(Path), } /// An [`inset()`](https://www.w3.org/TR/css-shapes-1/#funcdef-inset) rectangle shape. @@ -124,6 +129,19 @@ pub struct Point { y: LengthPercentage, } +/// An SVG path within a `path()` shape. +/// +/// See [Path](oxvg_path::Path). +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] +pub struct Path { + /// The fill rule used to determine the interior of the polygon. + pub fill_rule: FillRule, + /// The path data of each command in the path + pub path: oxvg_path::Path, +} + enum_property! { /// A [``](https://www.w3.org/TR/css-shapes-1/#typedef-fill-rule) used to /// determine the interior of a `polygon()` shape. @@ -152,6 +170,7 @@ impl<'i> Parse<'i> for BasicShape { "circle" => Ok(BasicShape::Circle(input.parse_nested_block(Circle::parse)?)), "ellipse" => Ok(BasicShape::Ellipse(input.parse_nested_block(Ellipse::parse)?)), "polygon" => Ok(BasicShape::Polygon(input.parse_nested_block(Polygon::parse)?)), + "path" => Ok(BasicShape::Path(input.parse_nested_block(Path::parse)?)), _ => Err(location.new_unexpected_token_error(Token::Ident(f.clone()))), } } @@ -233,6 +252,28 @@ impl<'i> Parse<'i> for Point { } } +impl<'i> Parse<'i> for Path { + fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { + use oxvg_path::convert; + + let fill_rule = input.try_parse(FillRule::parse); + if fill_rule.is_ok() { + input.expect_comma()?; + } + + let string = input.expect_string()?.to_string(); + let Ok(path) = oxvg_path::Path::parse(string) else { + return Err(input.new_custom_error(ParserError::InvalidValue)); + }; + let path = convert::run(&path, &convert::Options::default(), &convert::StyleInfo::conservative()); + + Ok(Path { + fill_rule: fill_rule.unwrap_or_default(), + path, + }) + } +} + impl ToCss for BasicShape { fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where @@ -259,6 +300,11 @@ impl ToCss for BasicShape { poly.to_css(dest)?; dest.write_char(')') } + BasicShape::Path(path) => { + dest.write_str("path(")?; + path.to_css(dest)?; + dest.write_char(')') + } } } } @@ -359,3 +405,29 @@ impl ToCss for Point { self.y.to_css(dest) } } + +impl ToCss for Path { + fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> + where + W: std::fmt::Write, + { + if self.fill_rule != FillRule::default() { + self.fill_rule.to_css(dest)?; + dest.delim(',', false)?; + } + + dest.write_char('"')?; + write!(dest, "{}", self.path)?; + dest.write_char('"')?; + Ok(()) + } +} + +#[cfg(feature = "visitor")] +#[cfg_attr(docsrs, doc(cfg(feature = "visitor")))] +impl<'i, V: ?Sized + Visitor<'i, T>, T: Visit<'i, T, V>> Visit<'i, T, V> for Path { + const CHILD_TYPES: VisitTypes = VisitTypes::empty(); + fn visit_children(&mut self, _: &mut V) -> Result<(), V::Error> { + Ok(()) + } +}