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