diff --git a/Cargo.lock b/Cargo.lock index db6c59ae..7358e44c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -153,7 +153,7 @@ dependencies = [ "anyhow", "chrono", "either", - "indexmap 2.2.2", + "indexmap 2.2.6", "itertools 0.12.1", "nom", "once_cell", @@ -658,9 +658,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.2" +version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824b2ae422412366ba479e8111fd301f7b5faece8149317bb81925979a53f520" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", "hashbrown 0.14.2", @@ -775,6 +775,7 @@ dependencies = [ "dashmap", "data-encoding", "getrandom", + "indexmap 2.2.6", "indoc", "itertools 0.10.5", "jemallocator", diff --git a/Cargo.toml b/Cargo.toml index 4e27dbf5..795b25ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,6 +62,7 @@ const-str = "0.3.1" pathdiff = "0.2.1" ahash = "0.8.7" paste = "1.0.12" +indexmap = "2.2.6" # CLI deps atty = { version = "0.2", optional = true } clap = { version = "3.0.6", features = ["derive"], optional = true } diff --git a/node/ast.d.ts b/node/ast.d.ts index 25c9dd7f..bd6e0e98 100644 --- a/node/ast.d.ts +++ b/node/ast.d.ts @@ -2066,6 +2066,12 @@ export type PropertyId = property: "text-size-adjust"; vendorPrefix: VendorPrefix; } + | { + property: "direction"; + } + | { + property: "unicode-bidi"; + } | { property: "box-decoration-break"; vendorPrefix: VendorPrefix; @@ -3445,6 +3451,14 @@ export type Declaration = value: TextSizeAdjust; vendorPrefix: VendorPrefix; } + | { + property: "direction"; + value: Direction2; + } + | { + property: "unicode-bidi"; + value: UnicodeBidi; + } | { property: "box-decoration-break"; value: BoxDecorationBreak; @@ -3757,6 +3771,10 @@ export type Declaration = property: "color-scheme"; value: ColorScheme; } + | { + property: "all"; + value: CSSWideKeyword; + } | { property: "unparsed"; value: UnparsedProperty; @@ -5729,6 +5747,14 @@ export type TextSizeAdjust = type: "percentage"; value: number; }; +/** + * A value for the [direction](https://drafts.csswg.org/css-writing-modes-3/#direction) property. + */ +export type Direction2 = "ltr" | "rtl"; +/** + * A value for the [unicode-bidi](https://drafts.csswg.org/css-writing-modes-3/#unicode-bidi) property. + */ +export type UnicodeBidi = "normal" | "embed" | "isolate" | "bidi-override" | "isolate-override" | "plaintext"; /** * A value for the [box-decoration-break](https://www.w3.org/TR/css-break-3/#break-decoration) property. */ @@ -6234,6 +6260,10 @@ export type ContainerNameList = type: "names"; value: String[]; }; +/** + * A [CSS-wide keyword](https://drafts.csswg.org/css-cascade-5/#defaulting-keywords). + */ +export type CSSWideKeyword = "initial" | "inherit" | "unset" | "revert" | "revert-layer"; /** * A CSS custom property name. */ @@ -7040,8 +7070,8 @@ export type ParsedComponent = }; } | { - type: "token"; - value: Token; + type: "token-list"; + value: TokenOrValue[]; }; /** * A [multiplier](https://drafts.css-houdini.org/css-properties-values-api/#multipliers) for a [SyntaxComponent](SyntaxComponent). Indicates whether and how the component may be repeated. diff --git a/src/declaration.rs b/src/declaration.rs index 556fd152..501e2f8c 100644 --- a/src/declaration.rs +++ b/src/declaration.rs @@ -1,7 +1,6 @@ //! CSS declarations. use std::borrow::Cow; -use std::collections::HashMap; use std::ops::Range; use crate::context::{DeclarationContext, PropertyHandlerContext}; @@ -11,6 +10,7 @@ use crate::printer::Printer; use crate::properties::box_shadow::BoxShadowHandler; use crate::properties::custom::{CustomProperty, CustomPropertyName}; use crate::properties::masking::MaskHandler; +use crate::properties::text::{Direction, UnicodeBidi}; use crate::properties::{ align::AlignHandler, animation::AnimationHandler, @@ -33,13 +33,14 @@ use crate::properties::{ transition::TransitionHandler, ui::ColorSchemeHandler, }; -use crate::properties::{Property, PropertyId}; +use crate::properties::{CSSWideKeyword, Property, PropertyId}; use crate::traits::{PropertyHandler, ToCss}; use crate::values::ident::DashedIdent; use crate::values::string::CowArcStr; #[cfg(feature = "visitor")] use crate::visitor::Visit; use cssparser::*; +use indexmap::IndexMap; /// A CSS declaration block. /// @@ -515,7 +516,10 @@ pub(crate) struct DeclarationHandler<'i> { color_scheme: ColorSchemeHandler, fallback: FallbackHandler, prefix: PrefixHandler, - custom_properties: HashMap, usize>, + all: Option, + direction: Option, + unicode_bidi: Option, + custom_properties: IndexMap, CustomProperty<'i>>, decls: DeclarationList<'i>, } @@ -552,6 +556,7 @@ impl<'i> DeclarationHandler<'i> { || self.color_scheme.handle_property(property, &mut self.decls, context) || self.fallback.handle_property(property, &mut self.decls, context) || self.prefix.handle_property(property, &mut self.decls, context) + || self.handle_all(property) || self.handle_custom_property(property, context) } @@ -566,18 +571,13 @@ impl<'i> DeclarationHandler<'i> { } if let CustomPropertyName::Custom(name) = &custom.name { - if let Some(index) = self.custom_properties.get(name) { - if self.decls[*index] == *property { + if let Some(prev) = self.custom_properties.get_mut(name) { + if prev.value == custom.value { return true; } - let mut custom = custom.clone(); - self.add_conditional_fallbacks(&mut custom, context); - self.decls[*index] = Property::Custom(custom); + *prev = custom.clone(); } else { - self.custom_properties.insert(name.clone(), self.decls.len()); - let mut custom = custom.clone(); - self.add_conditional_fallbacks(&mut custom, context); - self.decls.push(Property::Custom(custom)); + self.custom_properties.insert(name.clone(), custom.clone()); } return true; @@ -587,6 +587,32 @@ impl<'i> DeclarationHandler<'i> { false } + fn handle_all(&mut self, property: &Property<'i>) -> bool { + // The `all` property resets all properies except `unicode-bidi`, `direction`, and custom properties. + // https://drafts.csswg.org/css-cascade-5/#all-shorthand + match property { + Property::UnicodeBidi(bidi) => { + self.unicode_bidi = Some(*bidi); + true + } + Property::Direction(direction) => { + self.direction = Some(*direction); + true + } + Property::All(keyword) => { + *self = DeclarationHandler { + custom_properties: std::mem::take(&mut self.custom_properties), + unicode_bidi: self.unicode_bidi.clone(), + direction: self.direction.clone(), + all: Some(keyword.clone()), + ..Default::default() + }; + true + } + _ => false, + } + } + fn add_conditional_fallbacks( &self, custom: &mut CustomProperty<'i>, @@ -607,6 +633,21 @@ impl<'i> DeclarationHandler<'i> { } pub fn finalize(&mut self, context: &mut PropertyHandlerContext<'i, '_>) { + // Always place the `all` property first. Previous properties will have been omitted. + if let Some(all) = std::mem::take(&mut self.all) { + self.decls.push(Property::All(all)); + } + if let Some(direction) = std::mem::take(&mut self.direction) { + self.decls.push(Property::Direction(direction)); + } + if let Some(unicode_bidi) = std::mem::take(&mut self.unicode_bidi) { + self.decls.push(Property::UnicodeBidi(unicode_bidi)); + } + for (_, mut value) in std::mem::take(&mut self.custom_properties) { + self.add_conditional_fallbacks(&mut value, context); + self.decls.push(Property::Custom(value)); + } + self.background.finalize(&mut self.decls, context); self.border.finalize(&mut self.decls, context); self.outline.finalize(&mut self.decls, context); @@ -634,6 +675,5 @@ impl<'i> DeclarationHandler<'i> { self.color_scheme.finalize(&mut self.decls, context); self.fallback.finalize(&mut self.decls, context); self.prefix.finalize(&mut self.decls, context); - self.custom_properties.clear(); } } diff --git a/src/lib.rs b/src/lib.rs index f13b7c48..dd3b0fe8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21435,26 +21435,26 @@ mod tests { indoc! {r#" @keyframes foo { from { - --custom: #ff0; opacity: 0; + --custom: #ff0; } to { - --custom: #ee00be; opacity: 1; + --custom: #ee00be; } } @supports (color: lab(0% 0 0)) { @keyframes foo { from { - --custom: #ff0; opacity: 0; + --custom: #ff0; } to { - --custom: lab(50.998% 125.506 -50.7078); opacity: 1; + --custom: lab(50.998% 125.506 -50.7078); } } } @@ -23457,8 +23457,8 @@ mod tests { } .EgL3uq_foo { - --foo: red; color: var(--foo); + --foo: red; } "#}, map! { @@ -23507,10 +23507,10 @@ mod tests { } .EgL3uq_foo { - --EgL3uq_foo: red; - --EgL3uq_bar: green; color: var(--EgL3uq_foo); font-palette: --EgL3uq_Cooler; + --EgL3uq_foo: red; + --EgL3uq_bar: green; } .EgL3uq_bar { @@ -27244,4 +27244,31 @@ mod tests { }, ); } + + #[test] + fn test_all() { + minify_test(".foo { all: initial; all: initial }", ".foo{all:initial}"); + minify_test(".foo { all: initial; all: revert }", ".foo{all:revert}"); + minify_test(".foo { background: red; all: revert-layer }", ".foo{all:revert-layer}"); + minify_test( + ".foo { background: red; all: revert-layer; background: green }", + ".foo{all:revert-layer;background:green}", + ); + minify_test( + ".foo { --test: red; all: revert-layer }", + ".foo{all:revert-layer;--test:red}", + ); + minify_test( + ".foo { unicode-bidi: embed; all: revert-layer }", + ".foo{all:revert-layer;unicode-bidi:embed}", + ); + minify_test( + ".foo { direction: rtl; all: revert-layer }", + ".foo{all:revert-layer;direction:rtl}", + ); + minify_test( + ".foo { direction: rtl; all: revert-layer; direction: ltr }", + ".foo{all:revert-layer;direction:ltr}", + ); + } } diff --git a/src/properties/mod.rs b/src/properties/mod.rs index 845140f9..98756755 100644 --- a/src/properties/mod.rs +++ b/src/properties/mod.rs @@ -123,6 +123,7 @@ pub mod ui; use crate::declaration::DeclarationBlock; use crate::error::{ParserError, PrinterError}; use crate::logical::{LogicalGroup, PropertyCategory}; +use crate::macros::enum_property; use crate::parser::starts_with_ignore_ascii_case; use crate::parser::ParserOptions; use crate::prefixes::Feature; @@ -688,6 +689,8 @@ macro_rules! define_properties { $(#[$meta])* $property($type, $($vp)?), )+ + /// The [all](https://drafts.csswg.org/css-cascade-5/#all-shorthand) shorthand property. + All(CSSWideKeyword), /// An unparsed property. Unparsed(UnparsedProperty<'i>), /// A custom or unknown property. @@ -710,6 +713,7 @@ macro_rules! define_properties { } }, )+ + PropertyId::All => return Ok(Property::All(CSSWideKeyword::parse(input)?)), PropertyId::Custom(name) => return Ok(Property::Custom(CustomProperty::parse(name, input, options)?)), _ => {} }; @@ -731,6 +735,7 @@ macro_rules! define_properties { $(#[$meta])* $property(_, $(vp_name!($vp, p))?) => PropertyId::$property$((*vp_name!($vp, p)))?, )+ + All(_) => PropertyId::All, Unparsed(unparsed) => unparsed.property_id.clone(), Custom(custom) => PropertyId::Custom(custom.name.clone()) } @@ -779,6 +784,7 @@ macro_rules! define_properties { val.to_css(dest) } )+ + All(keyword) => keyword.to_css(dest), Unparsed(unparsed) => { unparsed.value.to_css(dest, false) } @@ -838,6 +844,7 @@ macro_rules! define_properties { ($name, get_prefix!($($vp)?)) }, )+ + All(_) => ("all", VendorPrefix::None), Unparsed(unparsed) => { let mut prefix = unparsed.property_id.prefix(); if prefix.is_empty() { @@ -966,7 +973,10 @@ macro_rules! define_properties { s.serialize_field("value", value)?; } )+ - _ => unreachable!() + All(value) => { + s.serialize_field("value", value)?; + } + Unparsed(_) | Custom(_) => unreachable!() } s.end() @@ -1072,7 +1082,10 @@ macro_rules! define_properties { Ok(Property::Custom(value)) } } - PropertyId::All => unreachable!() + PropertyId::All => { + let value = CSSWideKeyword::deserialize(deserializer)?; + Ok(Property::All(value)) + } } } } @@ -1134,6 +1147,17 @@ macro_rules! define_properties { with_prefix!($($vp)?) }, )+ + { + property!("all"); + #[derive(schemars::JsonSchema)] + struct T { + #[schemars(rename = "property", schema_with = "property")] + _property: u8, + #[schemars(rename = "value")] + _value: CSSWideKeyword + } + T::json_schema(gen) + }, { property!("unparsed"); @@ -1513,6 +1537,10 @@ define_properties! { // https://w3c.github.io/csswg-drafts/css-size-adjust/ "text-size-adjust": TextSizeAdjust(TextSizeAdjust, VendorPrefix) / WebKit / Moz / Ms, + // https://drafts.csswg.org/css-writing-modes-3/ + "direction": Direction(Direction), + "unicode-bidi": UnicodeBidi(UnicodeBidi), + // https://www.w3.org/TR/css-break-3/ "box-decoration-break": BoxDecorationBreak(BoxDecorationBreak, VendorPrefix) / WebKit, @@ -1667,3 +1695,19 @@ impl ToCss for Vec { Ok(()) } } + +enum_property! { + /// A [CSS-wide keyword](https://drafts.csswg.org/css-cascade-5/#defaulting-keywords). + pub enum CSSWideKeyword { + /// The property's initial value. + "initial": Initial, + /// The property's computed value on the parent element. + "inherit": Inherit, + /// Either inherit or initial depending on whether the property is inherited. + "unset": Unset, + /// Rolls back the cascade to the cascaded value of the earlier origin. + "revert": Revert, + /// Rolls back the cascade to the value of the previous cascade layer. + "revert-layer": RevertLayer, + } +} diff --git a/src/properties/prefix_handler.rs b/src/properties/prefix_handler.rs index 78dd7d09..7ad2dfe8 100644 --- a/src/properties/prefix_handler.rs +++ b/src/properties/prefix_handler.rs @@ -141,12 +141,6 @@ macro_rules! define_fallbacks { (val, paste::paste! { &mut self.[<$name:snake>] }) } )+ - PropertyId::All => { - let mut unparsed = val.clone(); - context.add_unparsed_fallbacks(&mut unparsed); - dest.push(Property::Unparsed(unparsed)); - return true - }, _ => return false }; diff --git a/src/properties/text.rs b/src/properties/text.rs index 4c7b6fde..e22f6fab 100644 --- a/src/properties/text.rs +++ b/src/properties/text.rs @@ -1599,3 +1599,31 @@ impl FallbackValues for SmallVec<[TextShadow; 1]> { res } } + +enum_property! { + /// A value for the [direction](https://drafts.csswg.org/css-writing-modes-3/#direction) property. + pub enum Direction { + /// This value sets inline base direction (bidi directionality) to line-left-to-line-right. + Ltr, + /// This value sets inline base direction (bidi directionality) to line-right-to-line-left. + Rtl, + } +} + +enum_property! { + /// A value for the [unicode-bidi](https://drafts.csswg.org/css-writing-modes-3/#unicode-bidi) property. + pub enum UnicodeBidi { + /// The box does not open an additional level of embedding. + "normal": Normal, + /// If the box is inline, this value creates a directional embedding by opening an additional level of embedding. + "embed": Embed, + /// On an inline box, this bidi-isolates its contents. + "isolate": Isolate, + /// This value puts the box’s immediate inline content in a directional override. + "bidi-override": BidiOverride, + /// This combines the isolation behavior of isolate with the directional override behavior of bidi-override. + "isolate-override": IsolateOverride, + /// This value behaves as isolate except that the base directionality is determined using a heuristic rather than the direction property. + "plaintext": Plaintext, + } +}