Skip to main content

css_ast/rules/container/
mod.rs

1#[cfg(feature = "visitable")]
2use crate::visit::{NodeId, QueryableNode};
3
4use super::prelude::*;
5
6mod features;
7pub use features::*;
8
9/// <https://drafts.csswg.org/css-contain-3/#container-rule>
10#[node]
11#[derive(Parse, Peek, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
12#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
13#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit, queryable(skip))]
14#[cfg_attr(feature = "css_feature_data", derive(::csskit_derives::ToCSSFeature), css_feature("css.at-rules.container"))]
15#[derive(csskit_derives::NodeWithMetadata)]
16#[metadata(node_kinds = AtRule, used_at_rules = Container)]
17pub struct ContainerRule<'a> {
18	#[cfg_attr(feature = "visitable", visit(skip))]
19	#[atom(CssAtomSet::Container)]
20	pub name: T![AtKeyword],
21	pub prelude: ContainerConditionList<'a>,
22	#[metadata(block)]
23	pub block: ContainerRulesBlock<'a>,
24}
25
26#[cfg(feature = "visitable")]
27impl<'a> QueryableNode for ContainerRule<'a> {
28	const NODE_ID: NodeId = NodeId::ContainerRule;
29}
30
31#[node]
32#[derive(Parse, ToSpan, ToCursors, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
33#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
34#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable))]
35#[derive(csskit_derives::NodeWithMetadata)]
36pub struct ContainerRulesBlock<'a>(pub Block<'a, StyleValue<'a>, Rule<'a>, CssMetadata>);
37
38#[node]
39#[derive(Peek, Parse, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
40#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
41#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable))]
42#[derive(csskit_derives::NodeWithMetadata)]
43pub struct ContainerConditionList<'a>(pub CommaSeparated<'a, ContainerCondition<'a>, 1>);
44
45#[node]
46#[derive(ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
47#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
48#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable))]
49#[derive(csskit_derives::NodeWithMetadata)]
50pub struct ContainerCondition<'a> {
51	#[cfg_attr(feature = "visitable", visit(skip))]
52	pub name: Option<T![Ident]>,
53	pub condition: Option<ContainerQuery<'a>>,
54}
55
56impl<'a> Peek<'a> for ContainerCondition<'a> {
57	const PEEK_KINDSET: KindSet = KindSet::new(&[Kind::Ident, Kind::LeftParen, Kind::Function]);
58}
59
60impl<'a> Parse<'a> for ContainerCondition<'a> {
61	fn parse<I>(p: &mut Parser<'a, I>) -> ParserResult<Self>
62	where
63		I: Iterator<Item = Cursor> + Clone,
64	{
65		let mut name = None;
66		let c = p.peek_n(1);
67		if c == Kind::Ident {
68			match p.to_atom::<CssAtomSet>(c) {
69				CssAtomSet::None | CssAtomSet::And | CssAtomSet::Not | CssAtomSet::Or => {}
70				_ => {
71					name = Some(p.parse::<T![Ident]>()?);
72				}
73			}
74		}
75		let condition =
76			if name.is_none() { Some(p.parse::<ContainerQuery>()?) } else { p.parse_if_peek::<ContainerQuery>()? };
77		Ok(Self { name, condition })
78	}
79}
80
81#[node]
82#[derive(ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
83#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
84#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit)]
85#[derive(csskit_derives::NodeWithMetadata)]
86pub enum ContainerQuery<'a> {
87	Is(ContainerFeature<'a>),
88	Not(T![Ident], ContainerFeature<'a>),
89	And(Vec<'a, (ContainerFeature<'a>, Option<T![Ident]>)>),
90	Or(Vec<'a, (ContainerFeature<'a>, Option<T![Ident]>)>),
91}
92
93impl<'a> Peek<'a> for ContainerQuery<'a> {
94	const PEEK_KINDSET: KindSet = ContainerFeature::PEEK_KINDSET.combine(<T![Ident]>::PEEK_KINDSET);
95
96	#[inline(always)]
97	fn peek<I>(p: &Parser<'a, I>, c: Cursor) -> bool
98	where
99		I: Iterator<Item = Cursor> + Clone,
100	{
101		<ContainerFeature>::peek(p, c) || (<T![Ident]>::peek(p, c) && p.to_atom::<CssAtomSet>(c) == CssAtomSet::Not)
102	}
103}
104
105impl<'a> Parse<'a> for ContainerQuery<'a> {
106	fn parse<I>(p: &mut Parser<'a, I>) -> ParserResult<Self>
107	where
108		I: Iterator<Item = Cursor> + Clone,
109	{
110		Self::parse_condition(p)
111	}
112}
113
114impl<'a> FeatureConditionList<'a> for ContainerQuery<'a> {
115	type FeatureCondition = ContainerFeature<'a>;
116	fn keyword_is_not<I>(p: &Parser<'a, I>, c: Cursor) -> bool
117	where
118		I: Iterator<Item = Cursor> + Clone,
119	{
120		p.equals_atom(c, &CssAtomSet::Not)
121	}
122	fn keyword_is_and<I>(p: &Parser<'a, I>, c: Cursor) -> bool
123	where
124		I: Iterator<Item = Cursor> + Clone,
125	{
126		p.equals_atom(c, &CssAtomSet::And)
127	}
128	fn keyword_is_or<I>(p: &Parser<'a, I>, c: Cursor) -> bool
129	where
130		I: Iterator<Item = Cursor> + Clone,
131	{
132		p.equals_atom(c, &CssAtomSet::Or)
133	}
134	fn build_is(feature: ContainerFeature<'a>) -> Self {
135		Self::Is(feature)
136	}
137	fn build_not(keyword: T![Ident], feature: ContainerFeature<'a>) -> Self {
138		Self::Not(keyword, feature)
139	}
140	fn build_and(feature: Vec<'a, (ContainerFeature<'a>, Option<T![Ident]>)>) -> Self {
141		Self::And(feature)
142	}
143	fn build_or(feature: Vec<'a, (ContainerFeature<'a>, Option<T![Ident]>)>) -> Self {
144		Self::Or(feature)
145	}
146}
147
148macro_rules! container_feature {
149	( $($name: ident($typ: ty))+ ) => {
150		#[node]
151		#[derive(ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
152		#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
153		#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit)]
154		#[derive(csskit_derives::NodeWithMetadata)]
155		pub enum ContainerFeature<'a> {
156			$($name($typ),)+
157			Style(
158				#[cfg_attr(feature = "visitable", visit(skip))] T![Function],
159				StyleQuery<'a>,
160				#[cfg_attr(feature = "visitable", visit(skip))] #[semantic_eq(skip)] T![')'],
161			),
162			ScrollState(
163				#[cfg_attr(feature = "visitable", visit(skip))] T![Function],
164				ScrollStateQuery<'a>,
165				#[cfg_attr(feature = "visitable", visit(skip))] #[semantic_eq(skip)] T![')'],
166			),
167		}
168	}
169}
170
171apply_container_features!(container_feature);
172
173impl<'a> Peek<'a> for ContainerFeature<'a> {
174	const PEEK_KINDSET: KindSet = KindSet::new(&[Kind::LeftParen, Kind::Function]);
175
176	#[inline(always)]
177	fn peek<I>(p: &Parser<'a, I>, c: Cursor) -> bool
178	where
179		I: Iterator<Item = Cursor> + Clone,
180	{
181		let c2 = p.peek_n(2);
182		(c == Kind::LeftParen && c2 == KindSet::new(&[Kind::Ident, Kind::Dimension]))
183			|| (c == Kind::Function
184				&& matches!(p.to_atom::<CssAtomSet>(c), CssAtomSet::Style | CssAtomSet::ScrollState))
185	}
186}
187
188impl<'a> Parse<'a> for ContainerFeature<'a> {
189	fn parse<I>(p: &mut Parser<'a, I>) -> ParserResult<Self>
190	where
191		I: Iterator<Item = Cursor> + Clone,
192	{
193		if p.peek::<T![Function]>() {
194			let function = p.parse::<T![Function]>()?;
195			return match p.to_atom::<CssAtomSet>(function.into()) {
196				CssAtomSet::Style => {
197					let query = p.parse::<StyleQuery>()?;
198					let close = p.parse::<T![')']>()?;
199					Ok(Self::Style(function, query, close))
200				}
201				CssAtomSet::ScrollState => {
202					let query = p.parse::<ScrollStateQuery>()?;
203					let close = p.parse::<T![')']>()?;
204					Ok(Self::ScrollState(function, query, close))
205				}
206				_ => Err(Diagnostic::new(function.into(), Diagnostic::unexpected_function))?,
207			};
208		}
209		let mut c = p.peek_n(2);
210		macro_rules! match_feature {
211			( $($name: ident($typ: ty))+ ) => {
212				// Only peek at the token as the underlying media feature parser needs to parse the leading keyword.
213				{
214					match p.to_atom::<CssAtomSet>(c) {
215						$(CssAtomSet::$name => {
216							let value = <$typ>::parse(p)?;
217							Self::$name(value)
218						},)+
219						_ => Err(Diagnostic::new(c, Diagnostic::unexpected))?
220					}
221				}
222			}
223		}
224		if c == Kind::Ident {
225			Ok(apply_container_features!(match_feature))
226		} else {
227			// Styles like (1em < width < 1em) or (1em <= width <= 1em)
228			c = p.peek_n(3);
229			if c != Kind::Ident {
230				c = p.peek_n(4)
231			}
232			Ok(apply_container_features!(match_feature))
233		}
234	}
235}
236
237macro_rules! apply_container_features {
238	($macro: ident) => {
239		$macro! {
240			// https://drafts.csswg.org/css-conditional-5/#container-features
241			Width(WidthContainerFeature)
242			Height(HeightContainerFeature)
243			InlineSize(InlineSizeContainerFeature)
244			BlockSize(BlockSizeContainerFeature)
245			AspectRatio(AspectRatioContainerFeature<'a>)
246			Orientation(OrientationContainerFeature)
247		}
248	};
249}
250use apply_container_features;
251
252#[cfg(test)]
253mod tests {
254	use super::*;
255	use crate::CssAtomSet;
256	use css_parse::assert_parse;
257
258	#[test]
259	fn test_writes() {
260		assert_parse!(CssAtomSet::ATOMS, ContainerQuery, "(width:2px)");
261		assert_parse!(CssAtomSet::ATOMS, ContainerCondition, "(width:2px)");
262		assert_parse!(CssAtomSet::ATOMS, ContainerCondition, "(inline-size>30em)");
263		assert_parse!(CssAtomSet::ATOMS, ContainerCondition, "(1em<width<1em)");
264		assert_parse!(CssAtomSet::ATOMS, ContainerCondition, "(width > 400px)");
265		assert_parse!(CssAtomSet::ATOMS, ContainerCondition, "--container");
266		assert_parse!(CssAtomSet::ATOMS, ContainerCondition, "--container (width > 400px)");
267		assert_parse!(CssAtomSet::ATOMS, ContainerConditionList, "(width > 400px), --container (width > 400px)");
268		assert_parse!(CssAtomSet::ATOMS, ContainerRule, "@container foo{}");
269		assert_parse!(CssAtomSet::ATOMS, ContainerRule, "@container foo (width:2px){}");
270		assert_parse!(CssAtomSet::ATOMS, ContainerRule, "@container foo (10em<width<10em){}");
271		assert_parse!(CssAtomSet::ATOMS, ContainerRule, "@container foo (width:2px){body{color:black}}");
272		// Style queries
273		assert_parse!(CssAtomSet::ATOMS, ContainerFeature, "style(--x:10px)");
274		assert_parse!(CssAtomSet::ATOMS, ContainerFeature, "style(--x: 10px)");
275		assert_parse!(CssAtomSet::ATOMS, ContainerFeature, "style(--x)");
276		assert_parse!(CssAtomSet::ATOMS, ContainerQuery, "style(--x:10px)");
277		assert_parse!(CssAtomSet::ATOMS, ContainerQuery, "style(--x)");
278		assert_parse!(CssAtomSet::ATOMS, ContainerCondition, "style(--x:10px)");
279		assert_parse!(CssAtomSet::ATOMS, ContainerCondition, "style(--x: 10px)");
280		assert_parse!(CssAtomSet::ATOMS, ContainerCondition, "style(--x)");
281		assert_parse!(CssAtomSet::ATOMS, ContainerRule, "@container style(--x:10px){}");
282		assert_parse!(CssAtomSet::ATOMS, ContainerRule, "@container style(--x: 10px){}");
283		assert_parse!(CssAtomSet::ATOMS, ContainerRule, "@container style(--x){}");
284		assert_parse!(CssAtomSet::ATOMS, ContainerRule, "@container style(--x:10px){body{color:green}}");
285		assert_parse!(CssAtomSet::ATOMS, ContainerRule, "@container style(--x: 10px){body{color:green}}");
286		assert_parse!(CssAtomSet::ATOMS, ContainerRule, "@container foo style(--x:10px){}");
287		// Scroll-state queries
288		assert_parse!(CssAtomSet::ATOMS, ContainerFeature, "scroll-state(stuck:top)");
289		assert_parse!(CssAtomSet::ATOMS, ContainerFeature, "scroll-state(scrollable:y and snapped:block)");
290		assert_parse!(CssAtomSet::ATOMS, ContainerRule, "@container sticky scroll-state(stuck: top){}");
291	}
292
293	#[test]
294	fn test_style_queries() {
295		// Basic style query
296		assert_parse!(CssAtomSet::ATOMS, ContainerFeature, "style(color:red)");
297		assert_parse!(CssAtomSet::ATOMS, ContainerFeature, "style(--my-var:10px)");
298		// Style query in container condition
299		assert_parse!(CssAtomSet::ATOMS, ContainerCondition, "style(color:red)");
300		assert_parse!(CssAtomSet::ATOMS, ContainerCondition, "style(--my-var:10px)");
301		// Style query in full container rule
302		assert_parse!(CssAtomSet::ATOMS, ContainerRule, "@container style(color:red){}");
303		assert_parse!(CssAtomSet::ATOMS, ContainerRule, "@container style(--my-var:10px){}");
304		// Named container with style query
305		assert_parse!(CssAtomSet::ATOMS, ContainerRule, "@container card style(color:red){}");
306		// Style query with rules inside
307		assert_parse!(CssAtomSet::ATOMS, ContainerRule, "@container style(color:red){body{color:black}}");
308	}
309
310	#[test]
311	fn test_scroll_state_queries() {
312		// Basic scroll-state query
313		assert_parse!(CssAtomSet::ATOMS, ContainerFeature, "scroll-state(scrollable:top)");
314		assert_parse!(CssAtomSet::ATOMS, ContainerFeature, "scroll-state(snapped:x)");
315		assert_parse!(CssAtomSet::ATOMS, ContainerFeature, "scroll-state(stuck:top)");
316		// Scroll-state query in container condition
317		assert_parse!(CssAtomSet::ATOMS, ContainerCondition, "scroll-state(stuck:top)");
318		// Scroll-state query in full container rule
319		assert_parse!(CssAtomSet::ATOMS, ContainerRule, "@container scroll-state(stuck:top){}");
320		assert_parse!(CssAtomSet::ATOMS, ContainerRule, "@container scroll-state(snapped:x){}");
321		// Named container with scroll-state query
322		assert_parse!(CssAtomSet::ATOMS, ContainerRule, "@container nav scroll-state(stuck:top){}");
323		// Scroll-state query with rules inside
324		assert_parse!(CssAtomSet::ATOMS, ContainerRule, "@container scroll-state(stuck:top){.item{opacity:0.5}}");
325	}
326}