Skip to main content

css_ast/types/
position.rs

1use super::prelude::*;
2use crate::LengthPercentage;
3use css_parse::Token;
4
5/// <https://drafts.csswg.org/css-values-4/#position>
6///
7/// ```text,ignore
8/// <position> = [
9///   [ left | center | right | top | bottom | <length-percentage> ]
10/// |
11///   [ left | center | right ] && [ top | center | bottom ]
12/// |
13///   [ left | center | right | <length-percentage> ]
14///   [ top | center | bottom | <length-percentage> ]
15/// |
16///   [ [ left | right ] <length-percentage> ] &&
17///   [ [ top | bottom ] <length-percentage> ]
18/// ]
19/// ```
20#[derive(ToCursors, ToSpan, SemanticEq, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
21#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
22#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
23#[derive(csskit_derives::NodeWithMetadata)]
24pub enum Position {
25	SingleValue(PositionSingleValue),
26	TwoValue(PositionHorizontal, PositionVertical),
27	FourValue(PositionHorizontalKeyword, LengthPercentage, PositionVerticalKeyword, LengthPercentage),
28}
29
30impl<'a> Peek<'a> for Position {
31	fn peek<I>(p: &Parser<'a, I>, c: Cursor) -> bool
32	where
33		I: Iterator<Item = Cursor> + Clone,
34	{
35		PositionSingleValue::peek(p, c)
36	}
37}
38
39impl<'a> Parse<'a> for Position {
40	fn parse<I>(p: &mut Parser<'a, I>) -> ParserResult<Self>
41	where
42		I: Iterator<Item = Cursor> + Clone,
43	{
44		let first = p.parse::<PositionSingleValue>()?;
45		// Single case
46		if !p.peek::<PositionSingleValue>() {
47			return Ok(Self::SingleValue(first));
48		}
49		let second = p.parse::<PositionSingleValue>()?;
50		// Two value
51		if !p.peek::<PositionSingleValue>() {
52			if let Some(horizontal) = first.to_horizontal() {
53				if let Some(vertical) = second.to_vertical() {
54					return Ok(Self::TwoValue(horizontal, vertical));
55				}
56			} else if let Some(horizontal) = second.to_horizontal() {
57				if let Some(vertical) = first.to_vertical() {
58					return Ok(Self::TwoValue(horizontal, vertical));
59				} else {
60					Err(Diagnostic::new(second.into(), Diagnostic::unexpected))?
61				}
62			}
63		}
64		// Four value
65		if matches!(first, PositionSingleValue::Center(_) | PositionSingleValue::LengthPercentage(_))
66			|| !matches!(&second, PositionSingleValue::LengthPercentage(_))
67		{
68			Err(Diagnostic::new(second.into(), Diagnostic::unexpected))?
69		}
70		let third = p.parse::<PositionSingleValue>()?;
71		if third.to_horizontal_keyword().is_none() && third.to_vertical_keyword().is_none() {
72			let cursor: Cursor = third.into();
73			Err(Diagnostic::new(cursor, Diagnostic::expected_ident))?
74		}
75		let fourth = p.parse::<LengthPercentage>()?;
76		if let PositionSingleValue::LengthPercentage(second) = second {
77			if let Some(horizontal) = first.to_horizontal_keyword() {
78				if let Some(vertical) = third.to_vertical_keyword() {
79					Ok(Self::FourValue(horizontal, second, vertical, fourth))
80				} else {
81					Err(Diagnostic::new(third.into(), Diagnostic::unexpected))?
82				}
83			} else if let Some(horizontal) = third.to_horizontal_keyword() {
84				if let Some(vertical) = first.to_vertical_keyword() {
85					Ok(Self::FourValue(horizontal, fourth, vertical, second))
86				} else {
87					Err(Diagnostic::new(third.into(), Diagnostic::unexpected))?
88				}
89			} else {
90				Err(Diagnostic::new(third.into(), Diagnostic::unexpected))?
91			}
92		} else {
93			Err(Diagnostic::new(second.into(), Diagnostic::unexpected))?
94		}
95	}
96}
97
98#[derive(Parse, Peek, IntoCursor, ToCursors, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
99#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
100pub enum PositionSingleValue {
101	#[atom(CssAtomSet::Left)]
102	Left(T![Ident]),
103	#[atom(CssAtomSet::Right)]
104	Right(T![Ident]),
105	#[atom(CssAtomSet::Center)]
106	Center(T![Ident]),
107	#[atom(CssAtomSet::Top)]
108	Top(T![Ident]),
109	#[atom(CssAtomSet::Bottom)]
110	Bottom(T![Ident]),
111	LengthPercentage(LengthPercentage),
112}
113
114impl PositionSingleValue {
115	#[inline]
116	fn to_horizontal(self) -> Option<PositionHorizontal> {
117		match self {
118			Self::Left(t) => Some(PositionHorizontal::Left(t)),
119			Self::Right(t) => Some(PositionHorizontal::Right(t)),
120			Self::Center(t) => Some(PositionHorizontal::Center(t)),
121			Self::LengthPercentage(l) => Some(PositionHorizontal::LengthPercentage(l)),
122			_ => None,
123		}
124	}
125
126	#[inline]
127	fn to_vertical(self) -> Option<PositionVertical> {
128		match self {
129			Self::Top(t) => Some(PositionVertical::Top(t)),
130			Self::Bottom(t) => Some(PositionVertical::Bottom(t)),
131			Self::Center(t) => Some(PositionVertical::Center(t)),
132			Self::LengthPercentage(l) => Some(PositionVertical::LengthPercentage(l)),
133			_ => None,
134		}
135	}
136
137	#[inline]
138	fn to_horizontal_keyword(self) -> Option<PositionHorizontalKeyword> {
139		match self {
140			Self::Left(t) => Some(PositionHorizontalKeyword::Left(t)),
141			Self::Right(t) => Some(PositionHorizontalKeyword::Right(t)),
142			_ => None,
143		}
144	}
145
146	#[inline]
147	fn to_vertical_keyword(self) -> Option<PositionVerticalKeyword> {
148		match self {
149			Self::Top(t) => Some(PositionVerticalKeyword::Top(t)),
150			Self::Bottom(t) => Some(PositionVerticalKeyword::Bottom(t)),
151			_ => None,
152		}
153	}
154}
155
156impl From<PositionSingleValue> for Kind {
157	fn from(value: PositionSingleValue) -> Self {
158		let t: Token = value.into();
159		t.into()
160	}
161}
162
163#[derive(IntoCursor, ToCursors, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
164#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
165pub enum PositionHorizontal {
166	Left(T![Ident]),
167	Right(T![Ident]),
168	Center(T![Ident]),
169	LengthPercentage(LengthPercentage),
170}
171
172#[derive(IntoCursor, ToCursors, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
173#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
174pub enum PositionVertical {
175	Top(T![Ident]),
176	Bottom(T![Ident]),
177	Center(T![Ident]),
178	LengthPercentage(LengthPercentage),
179}
180
181#[derive(Parse, Peek, IntoCursor, ToCursors, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
182#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
183#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(skip))]
184#[derive(csskit_derives::NodeWithMetadata)]
185pub enum PositionHorizontalKeyword {
186	#[atom(CssAtomSet::Left)]
187	Left(T![Ident]),
188	#[atom(CssAtomSet::Right)]
189	Right(T![Ident]),
190}
191
192#[derive(Parse, Peek, IntoCursor, ToCursors, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
193#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
194#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(skip))]
195#[derive(csskit_derives::NodeWithMetadata)]
196pub enum PositionVerticalKeyword {
197	#[atom(CssAtomSet::Top)]
198	Top(T![Ident]),
199	#[atom(CssAtomSet::Bottom)]
200	Bottom(T![Ident]),
201}
202
203#[cfg(test)]
204mod tests {
205	use crate::Length;
206
207	use super::*;
208	use crate::CssAtomSet;
209	use css_parse::{assert_parse, assert_parse_error, assert_parse_span};
210
211	#[test]
212	fn size_test() {
213		assert_eq!(std::mem::size_of::<Position>(), 64);
214	}
215
216	#[test]
217	fn test_writes() {
218		assert_parse!(CssAtomSet::ATOMS, Position, "left", Position::SingleValue(PositionSingleValue::Left(_)));
219		assert_parse!(CssAtomSet::ATOMS, Position, "right", Position::SingleValue(PositionSingleValue::Right(_)));
220		assert_parse!(CssAtomSet::ATOMS, Position, "top", Position::SingleValue(PositionSingleValue::Top(_)));
221		assert_parse!(CssAtomSet::ATOMS, Position, "bottom", Position::SingleValue(PositionSingleValue::Bottom(_)));
222		assert_parse!(CssAtomSet::ATOMS, Position, "center", Position::SingleValue(PositionSingleValue::Center(_)));
223		assert_parse!(
224			CssAtomSet::ATOMS,
225			Position,
226			"center center",
227			Position::TwoValue(PositionHorizontal::Center(_), PositionVertical::Center(_))
228		);
229		assert_parse!(
230			CssAtomSet::ATOMS,
231			Position,
232			"center top",
233			Position::TwoValue(PositionHorizontal::Center(_), PositionVertical::Top(_))
234		);
235		assert_parse!(
236			CssAtomSet::ATOMS,
237			Position,
238			"50% 50%",
239			Position::TwoValue(
240				PositionHorizontal::LengthPercentage(LengthPercentage::Percent(_)),
241				PositionVertical::LengthPercentage(LengthPercentage::Percent(_))
242			)
243		);
244		assert_parse!(
245			CssAtomSet::ATOMS,
246			Position,
247			"50%",
248			Position::SingleValue(PositionSingleValue::LengthPercentage(LengthPercentage::Percent(_)))
249		);
250		assert_parse!(
251			CssAtomSet::ATOMS,
252			Position,
253			"20px 30px",
254			Position::TwoValue(
255				PositionHorizontal::LengthPercentage(LengthPercentage::Length(Length::Px(_))),
256				PositionVertical::LengthPercentage(LengthPercentage::Length(Length::Px(_)))
257			)
258		);
259		assert_parse!(
260			CssAtomSet::ATOMS,
261			Position,
262			"2% bottom",
263			Position::TwoValue(
264				PositionHorizontal::LengthPercentage(LengthPercentage::Percent(_)),
265				PositionVertical::Bottom(_)
266			)
267		);
268		assert_parse!(
269			CssAtomSet::ATOMS,
270			Position,
271			"-70% -180%",
272			Position::TwoValue(
273				PositionHorizontal::LengthPercentage(LengthPercentage::Percent(_)),
274				PositionVertical::LengthPercentage(LengthPercentage::Percent(_))
275			)
276		);
277		assert_parse!(
278			CssAtomSet::ATOMS,
279			Position,
280			"right 8.5%",
281			Position::TwoValue(
282				PositionHorizontal::Right(_),
283				PositionVertical::LengthPercentage(LengthPercentage::Percent(_))
284			)
285		);
286		assert_parse!(
287			CssAtomSet::ATOMS,
288			Position,
289			"right -6px bottom 12vmin",
290			Position::FourValue(
291				PositionHorizontalKeyword::Right(_),
292				LengthPercentage::Length(Length::Px(_)),
293				PositionVerticalKeyword::Bottom(_),
294				LengthPercentage::Length(Length::Vmin(_))
295			)
296		);
297		assert_parse!(
298			CssAtomSet::ATOMS,
299			Position,
300			"bottom 12vmin right -6px",
301			Position::FourValue(
302				PositionHorizontalKeyword::Right(_),
303				LengthPercentage::Length(Length::Px(_)),
304				PositionVerticalKeyword::Bottom(_),
305				LengthPercentage::Length(Length::Vmin(_))
306			)
307		);
308	}
309
310	#[test]
311	fn test_errors() {
312		assert_parse_error!(CssAtomSet::ATOMS, Position, "left left");
313		assert_parse_error!(CssAtomSet::ATOMS, Position, "bottom top");
314		assert_parse_error!(CssAtomSet::ATOMS, Position, "10px 15px 20px 15px");
315		// 3 value syntax is not allowed
316		assert_parse_error!(CssAtomSet::ATOMS, Position, "right -6px bottom");
317	}
318
319	#[test]
320	fn test_spans() {
321		// Parsing should stop at var()
322		assert_parse_span!(
323			CssAtomSet::ATOMS,
324			Position,
325			r#"
326			right var(--foo)
327			^^^^^
328		"#
329		);
330		// Parsing should stop at four values:
331		assert_parse_span!(
332			CssAtomSet::ATOMS,
333			Position,
334			r#"
335			right -6px bottom 12rem 8px 20%
336			^^^^^^^^^^^^^^^^^^^^^^^
337		"#
338		);
339	}
340
341	// #[cfg(feature = "serde")]
342	// #[test]
343	// fn test_serializes() {
344	// 	assert_json!(Position, "center center", {
345	// 		"node": [
346	// 			{"type": "center"},
347	// 			{"type": "center"},
348	// 		],
349	// 		"start": 0,
350	// 		"end": 13
351	// 	});
352	// 	assert_json!(Position, "left bottom", {
353	// 		"node": [
354	// 			{"type": "left", "value": null},
355	// 			{"type": "bottom", "value": null},
356	// 		],
357	// 		"start": 0,
358	// 		"end": 11
359	// 	});
360	// }
361}