css_ast/types/
position.rs

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