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