From 00d4e511de19eeb2d583d70019a138e98e664e0e Mon Sep 17 00:00:00 2001 From: Noah Baldwin Date: Thu, 12 Dec 2024 19:48:13 +1100 Subject: [PATCH 1/6] Add path parsing to BasicShape --- Cargo.lock | 27 +++++++++++++++++++-------- Cargo.toml | 2 ++ src/values/shape.rs | 39 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cf4e300e..b1b9c43a 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,21 @@ 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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00fb8e08be2a3fcb2ff98de9122a82295dd6ef2cc9596f3c14a8c7fcf3d5a5bc" +dependencies = [ + "bitflags 2.6.0", + "ryu", +] + [[package]] name = "parcel_selectors" version = "0.28.0" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.6.0", "cssparser", "log", "phf", @@ -1330,7 +1341,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 +1350,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..0c042ba9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ 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.2" # CLI deps atty = { version = "0.2", optional = true } clap = { version = "3.0.6", features = ["derive"], optional = true } diff --git a/src/values/shape.rs b/src/values/shape.rs index 9cb5f585..f361addb 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; @@ -11,6 +13,7 @@ use crate::traits::{Parse, ToCss}; #[cfg(feature = "visitor")] use crate::visitor::Visit; 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,15 @@ pub struct Point { y: LengthPercentage, } +/// A path within a `path()` shape. +/// +/// See [Path](oxvg_path::Path). +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] +pub struct Path(pub 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 +166,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 +248,16 @@ impl<'i> Parse<'i> for Point { } } +impl<'i> Parse<'i> for Path { + fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { + let string = input.expect_string()?.to_string(); + match oxvg_path::Path::parse(string) { + Ok(path) => Ok(Path(path)), + Err(_) => Err(input.new_custom_error(ParserError::InvalidValue)), + } + } +} + impl ToCss for BasicShape { fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where @@ -259,6 +284,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 +389,12 @@ 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, + { + Ok(write!(dest, "{}", self.0)?) + } +} From f9bc7a50ac6b66ca00026fe6359f724bc4b0e36d Mon Sep 17 00:00:00 2001 From: Noah Baldwin Date: Thu, 12 Dec 2024 19:48:42 +1100 Subject: [PATCH 2/6] Bump oxvg version to include serde and jsonschema --- Cargo.lock | 6 ++++-- Cargo.toml | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b1b9c43a..b9e9db84 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -977,12 +977,14 @@ checksum = "7f222829ae9293e33a9f5e9f440c6760a3d450a64affe1846486b140db81c1f4" [[package]] name = "oxvg_path" -version = "0.0.1-beta.2" +version = "0.0.1-beta.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00fb8e08be2a3fcb2ff98de9122a82295dd6ef2cc9596f3c14a8c7fcf3d5a5bc" +checksum = "87e451f3fe3d6a997e83f0557b03b00175a946e41f155712eff9adc4088d3aac" dependencies = [ "bitflags 2.6.0", "ryu", + "schemars", + "serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 0c042ba9..4dafb59d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,7 +39,7 @@ 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", @@ -74,7 +74,7 @@ pathdiff = "0.2.1" ahash = "0.8.7" paste = "1.0.12" indexmap = "2.2.6" -oxvg_path = "0.0.1-beta.2" +oxvg_path = "0.0.1-beta.4" # CLI deps atty = { version = "0.2", optional = true } clap = { version = "3.0.6", features = ["derive"], optional = true } From 50758a8d927c4fa933594d0bfe333ccfbee388bf Mon Sep 17 00:00:00 2001 From: Noah Baldwin Date: Thu, 12 Dec 2024 19:49:09 +1100 Subject: [PATCH 3/6] Implement visitor for Path --- src/values/shape.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/values/shape.rs b/src/values/shape.rs index f361addb..36aa6ad6 100644 --- a/src/values/shape.rs +++ b/src/values/shape.rs @@ -11,7 +11,7 @@ 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; @@ -129,11 +129,10 @@ pub struct Point { y: LengthPercentage, } -/// A path within a `path()` shape. +/// An SVG path within a `path()` shape. /// /// See [Path](oxvg_path::Path). #[derive(Debug, Clone, PartialEq)] -#[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] pub struct Path(pub oxvg_path::Path); @@ -398,3 +397,12 @@ impl ToCss for Path { Ok(write!(dest, "{}", self.0)?) } } + +#[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(()) + } +} From 7c0fd14307fd011fafa245235c98cf129e88bd01 Mon Sep 17 00:00:00 2001 From: Noah Baldwin Date: Thu, 12 Dec 2024 19:49:39 +1100 Subject: [PATCH 4/6] Include fill rule in path parsing --- src/values/shape.rs | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/src/values/shape.rs b/src/values/shape.rs index 36aa6ad6..8bfe023f 100644 --- a/src/values/shape.rs +++ b/src/values/shape.rs @@ -135,7 +135,12 @@ pub struct Point { #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] -pub struct Path(pub oxvg_path::Path); +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 @@ -249,9 +254,17 @@ impl<'i> Parse<'i> for Point { impl<'i> Parse<'i> for Path { fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { + let fill_rule = input.try_parse(FillRule::parse); + if fill_rule.is_ok() { + input.expect_comma()?; + } + let string = input.expect_string()?.to_string(); match oxvg_path::Path::parse(string) { - Ok(path) => Ok(Path(path)), + Ok(path) => Ok(Path { + fill_rule: fill_rule.unwrap_or_default(), + path, + }), Err(_) => Err(input.new_custom_error(ParserError::InvalidValue)), } } @@ -394,7 +407,15 @@ impl ToCss for Path { where W: std::fmt::Write, { - Ok(write!(dest, "{}", self.0)?) + 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(()) } } From 283a9518d6f0ffcf4d655b06cc76eb740afb4a45 Mon Sep 17 00:00:00 2001 From: Noah Baldwin Date: Thu, 12 Dec 2024 19:50:00 +1100 Subject: [PATCH 5/6] Convert for optimal path data --- src/values/shape.rs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/values/shape.rs b/src/values/shape.rs index 8bfe023f..f158670e 100644 --- a/src/values/shape.rs +++ b/src/values/shape.rs @@ -254,19 +254,23 @@ 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(); - match oxvg_path::Path::parse(string) { - Ok(path) => Ok(Path { - fill_rule: fill_rule.unwrap_or_default(), - path, - }), - Err(_) => Err(input.new_custom_error(ParserError::InvalidValue)), - } + 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, + }) } } From 5be5e7c3af0312a31a695933bb4cd7bf02bbd74a Mon Sep 17 00:00:00 2001 From: Noah Baldwin Date: Thu, 12 Dec 2024 19:50:28 +1100 Subject: [PATCH 6/6] Add tests for clip-path paths --- src/lib.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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}",