Skip to main content

css_ast/types/
position.rs

1use super::prelude::*;
2use crate::LengthPercentage;
3
4/// <https://drafts.csswg.org/css-values-5/#typedef-position>
5///
6/// ```text,ignore
7/// <position> = <position-one> | <position-two> | <position-four>
8/// ```
9#[derive(ToCursors, ToSpan, SemanticEq, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
10#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
11#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
12#[derive(csskit_derives::NodeWithMetadata)]
13pub enum Position {
14	One(PositionOne),
15	Two(PositionTwo),
16	Four(PositionFour),
17}
18
19impl<'a> Peek<'a> for Position {
20	const PEEK_KINDSET: KindSet = PositionOne::PEEK_KINDSET;
21
22	#[inline(always)]
23	fn peek<I>(p: &Parser<'a, I>, c: Cursor) -> bool
24	where
25		I: Iterator<Item = Cursor> + Clone,
26	{
27		PositionOne::peek(p, c)
28	}
29}
30
31impl<'a> Parse<'a> for Position {
32	fn parse<I>(p: &mut Parser<'a, I>) -> ParserResult<Self>
33	where
34		I: Iterator<Item = Cursor> + Clone,
35	{
36		let first = p.parse::<PositionOne>()?;
37		if !p.peek::<PositionOne>() {
38			return Ok(Self::One(first));
39		}
40		let second = p.parse::<PositionOne>()?;
41		if !p.peek::<PositionOne>() {
42			return Ok(Self::Two(PositionTwo::from_two(p, first, second)?));
43		}
44		// Four-value: first two tokens must form a keyword + LP pair
45		Ok(Self::Four(PositionFour::from_four(p, first, second)?))
46	}
47}
48
49/// <https://drafts.csswg.org/css-values-5/#typedef-position-one>
50///
51/// ```text,ignore
52/// <position-one> = [
53///   left | center | right | top | bottom |
54///   x-start | x-end | y-start | y-end |
55///   block-start | block-end | inline-start | inline-end |
56///   start | end |
57///   <length-percentage>
58/// ]
59/// ```
60#[derive(
61	Parse, Peek, IntoCursor, ToSpan, SemanticEq, ToCursors, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash,
62)]
63#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
64#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
65#[derive(csskit_derives::NodeWithMetadata)]
66pub enum PositionOne {
67	#[atom(CssAtomSet::Left)]
68	Left(T![Ident]),
69	#[atom(CssAtomSet::Right)]
70	Right(T![Ident]),
71	#[atom(CssAtomSet::Center)]
72	Center(T![Ident]),
73	#[atom(CssAtomSet::Top)]
74	Top(T![Ident]),
75	#[atom(CssAtomSet::Bottom)]
76	Bottom(T![Ident]),
77	#[atom(CssAtomSet::XStart)]
78	XStart(T![Ident]),
79	#[atom(CssAtomSet::XEnd)]
80	XEnd(T![Ident]),
81	#[atom(CssAtomSet::YStart)]
82	YStart(T![Ident]),
83	#[atom(CssAtomSet::YEnd)]
84	YEnd(T![Ident]),
85	#[atom(CssAtomSet::BlockStart)]
86	BlockStart(T![Ident]),
87	#[atom(CssAtomSet::BlockEnd)]
88	BlockEnd(T![Ident]),
89	#[atom(CssAtomSet::InlineStart)]
90	InlineStart(T![Ident]),
91	#[atom(CssAtomSet::InlineEnd)]
92	InlineEnd(T![Ident]),
93	#[atom(CssAtomSet::Start)]
94	Start(T![Ident]),
95	#[atom(CssAtomSet::End)]
96	End(T![Ident]),
97	LengthPercentage(LengthPercentage),
98}
99
100impl PositionOne {
101	pub(crate) fn to_horizontal(self) -> Option<PositionHorizontal> {
102		match self {
103			Self::Left(t) => Some(PositionHorizontal::Left(t)),
104			Self::Right(t) => Some(PositionHorizontal::Right(t)),
105			Self::Center(t) => Some(PositionHorizontal::Center(t)),
106			Self::XStart(t) => Some(PositionHorizontal::XStart(t)),
107			Self::XEnd(t) => Some(PositionHorizontal::XEnd(t)),
108			Self::LengthPercentage(l) => Some(PositionHorizontal::LengthPercentage(l)),
109			_ => None,
110		}
111	}
112
113	pub(crate) fn to_vertical(self) -> Option<PositionVertical> {
114		match self {
115			Self::Top(t) => Some(PositionVertical::Top(t)),
116			Self::Bottom(t) => Some(PositionVertical::Bottom(t)),
117			Self::Center(t) => Some(PositionVertical::Center(t)),
118			Self::YStart(t) => Some(PositionVertical::YStart(t)),
119			Self::YEnd(t) => Some(PositionVertical::YEnd(t)),
120			Self::LengthPercentage(l) => Some(PositionVertical::LengthPercentage(l)),
121			_ => None,
122		}
123	}
124
125	pub(crate) 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			Self::XStart(t) => Some(PositionHorizontalKeyword::XStart(t)),
130			Self::XEnd(t) => Some(PositionHorizontalKeyword::XEnd(t)),
131			_ => None,
132		}
133	}
134
135	pub(crate) fn to_vertical_keyword(self) -> Option<PositionVerticalKeyword> {
136		match self {
137			Self::Top(t) => Some(PositionVerticalKeyword::Top(t)),
138			Self::Bottom(t) => Some(PositionVerticalKeyword::Bottom(t)),
139			Self::YStart(t) => Some(PositionVerticalKeyword::YStart(t)),
140			Self::YEnd(t) => Some(PositionVerticalKeyword::YEnd(t)),
141			_ => None,
142		}
143	}
144
145	pub(crate) fn to_block_axis(self) -> Option<PositionBlockAxis> {
146		match self {
147			Self::BlockStart(t) => Some(PositionBlockAxis::BlockStart(t)),
148			Self::BlockEnd(t) => Some(PositionBlockAxis::BlockEnd(t)),
149			Self::Center(t) => Some(PositionBlockAxis::Center(t)),
150			_ => None,
151		}
152	}
153
154	pub(crate) fn to_inline_axis(self) -> Option<PositionInlineAxis> {
155		match self {
156			Self::InlineStart(t) => Some(PositionInlineAxis::InlineStart(t)),
157			Self::InlineEnd(t) => Some(PositionInlineAxis::InlineEnd(t)),
158			Self::Center(t) => Some(PositionInlineAxis::Center(t)),
159			_ => None,
160		}
161	}
162
163	pub(crate) fn to_block_axis_keyword(self) -> Option<PositionBlockAxisKeyword> {
164		match self {
165			Self::BlockStart(t) => Some(PositionBlockAxisKeyword::BlockStart(t)),
166			Self::BlockEnd(t) => Some(PositionBlockAxisKeyword::BlockEnd(t)),
167			_ => None,
168		}
169	}
170
171	pub(crate) fn to_inline_axis_keyword(self) -> Option<PositionInlineAxisKeyword> {
172		match self {
173			Self::InlineStart(t) => Some(PositionInlineAxisKeyword::InlineStart(t)),
174			Self::InlineEnd(t) => Some(PositionInlineAxisKeyword::InlineEnd(t)),
175			_ => None,
176		}
177	}
178
179	pub(crate) fn to_logical(self) -> Option<PositionLogical> {
180		match self {
181			Self::Start(t) => Some(PositionLogical::Start(t)),
182			Self::End(t) => Some(PositionLogical::End(t)),
183			_ => None,
184		}
185	}
186}
187
188/// <https://drafts.csswg.org/css-values-5/#typedef-position-two>
189///
190/// ```text,ignore
191/// <position-two> = [
192///   [ left | center | right | x-start | x-end ] &&
193///   [ top | center | bottom | y-start | y-end ]
194/// |
195///   [ left | center | right | x-start | x-end | <lp> ]
196///   [ top | center | bottom | y-start | y-end | <lp> ]
197/// |
198///   [ block-start | center | block-end ] &&
199///   [ inline-start | center | inline-end ]
200/// |
201///   [ start | center | end ]{2}
202/// ]
203/// ```
204///
205/// All forms normalise to (primary-axis, secondary-axis) order in the AST.
206#[derive(ToCursors, ToSpan, SemanticEq, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
207#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
208#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
209#[derive(csskit_derives::NodeWithMetadata)]
210pub enum PositionTwo {
211	/// Physical horizontal/vertical axes, with optional `<length-percentage>`.
212	/// Stored horizontal-first regardless of source order.
213	Physical(PositionHorizontal, PositionVertical),
214	/// Flow-relative block/inline axes.
215	/// Stored block-first regardless of source order.
216	FlowRelative(PositionBlockAxis, PositionInlineAxis),
217	/// Axis-ambiguous `start`/`end` pair; first = block axis, second = inline axis.
218	Logical(PositionLogical, PositionLogical),
219}
220
221impl<'a> Peek<'a> for PositionTwo {
222	const PEEK_KINDSET: KindSet = PositionOne::PEEK_KINDSET;
223
224	#[inline(always)]
225	fn peek<I>(p: &Parser<'a, I>, c: Cursor) -> bool
226	where
227		I: Iterator<Item = Cursor> + Clone,
228	{
229		PositionOne::peek(p, c)
230	}
231}
232
233impl PositionTwo {
234	pub(crate) fn from_two<'a, I>(_p: &mut Parser<'a, I>, first: PositionOne, second: PositionOne) -> ParserResult<Self>
235	where
236		I: Iterator<Item = Cursor> + Clone,
237	{
238		// Try physical form: one must be horizontal, the other vertical
239		let h = first.to_horizontal();
240		let v = second.to_vertical();
241		if let (Some(h), Some(v)) = (h, v) {
242			return Ok(Self::Physical(h, v));
243		}
244		// Reversed physical (vertical first, horizontal second)
245		let v = first.to_vertical();
246		let h = second.to_horizontal();
247		if let (Some(v), Some(h)) = (v, h) {
248			return Ok(Self::Physical(h, v));
249		}
250		// Flow-relative: block && inline (reorderable)
251		let block = first.to_block_axis();
252		let inline = second.to_inline_axis();
253		if let (Some(block), Some(inline)) = (block, inline) {
254			return Ok(Self::FlowRelative(block, inline));
255		}
256		let inline = first.to_inline_axis();
257		let block = second.to_block_axis();
258		if let (Some(inline), Some(block)) = (inline, block) {
259			return Ok(Self::FlowRelative(block, inline));
260		}
261		// Logical: start|end pair
262		let a = first.to_logical();
263		let b = second.to_logical();
264		if let (Some(a), Some(b)) = (a, b) {
265			return Ok(Self::Logical(a, b));
266		}
267		Err(Diagnostic::new(second.into(), Diagnostic::unexpected))?
268	}
269}
270
271/// <https://drafts.csswg.org/css-values-5/#typedef-position-four>
272///
273/// ```text,ignore
274/// <position-four> = [
275///   [ [ left | right | x-start | x-end ] <lp> ] &&
276///   [ [ top | bottom | y-start | y-end ] <lp> ]
277/// |
278///   [ [ block-start | block-end ] <lp> ] &&
279///   [ [ inline-start | inline-end ] <lp> ]
280/// |
281///   [ [ start | end ] <lp> ]{2}
282/// ]
283/// ```
284///
285/// All forms stored with the first keyword-axis pair first.
286#[derive(ToCursors, ToSpan, SemanticEq, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
287#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
288#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
289#[derive(csskit_derives::NodeWithMetadata)]
290pub enum PositionFour {
291	/// `[left|right|x-start|x-end] <lp>` && `[top|bottom|y-start|y-end] <lp>`
292	/// Stored horizontal-first.
293	Physical(PositionHorizontalKeyword, LengthPercentage, PositionVerticalKeyword, LengthPercentage),
294	/// `[block-start|block-end] <lp>` && `[inline-start|inline-end] <lp>`
295	/// Stored block-first.
296	FlowRelative(PositionBlockAxisKeyword, LengthPercentage, PositionInlineAxisKeyword, LengthPercentage),
297	/// `[start|end] <lp>` × 2; first = block axis, second = inline axis.
298	Logical(PositionLogical, LengthPercentage, PositionLogical, LengthPercentage),
299}
300
301impl PositionFour {
302	pub(crate) fn from_four<'a, I>(p: &mut Parser<'a, I>, first: PositionOne, second: PositionOne) -> ParserResult<Self>
303	where
304		I: Iterator<Item = Cursor> + Clone,
305	{
306		// second must be a <length-percentage> in all four-value forms except
307		// when the pair is reversed (vertical/block/logical first).
308		// Pattern: <keyword> <lp> <keyword> <lp>  (or reversed)
309		let second_lp = if let PositionOne::LengthPercentage(lp) = second { Some(lp) } else { None };
310
311		if let Some(lp1) = second_lp {
312			let third = p.parse::<PositionOne>()?;
313			let fourth = p.parse::<LengthPercentage>()?;
314			// Physical: first=H keyword, third=V keyword
315			if let Some(h_kw) = first.to_horizontal_keyword()
316				&& let Some(v_kw) = third.to_vertical_keyword()
317			{
318				return Ok(Self::Physical(h_kw, lp1, v_kw, fourth));
319			}
320			// Physical reversed: first=V keyword, third=H keyword
321			if let Some(v_kw) = first.to_vertical_keyword()
322				&& let Some(h_kw) = third.to_horizontal_keyword()
323			{
324				return Ok(Self::Physical(h_kw, fourth, v_kw, lp1));
325			}
326			// Flow-relative: first=block keyword, third=inline keyword
327			if let Some(b_kw) = first.to_block_axis_keyword()
328				&& let Some(i_kw) = third.to_inline_axis_keyword()
329			{
330				return Ok(Self::FlowRelative(b_kw, lp1, i_kw, fourth));
331			}
332			// Flow-relative reversed: first=inline keyword, third=block keyword
333			if let Some(i_kw) = first.to_inline_axis_keyword()
334				&& let Some(b_kw) = third.to_block_axis_keyword()
335			{
336				return Ok(Self::FlowRelative(b_kw, fourth, i_kw, lp1));
337			}
338			// Logical: first=start|end, third=start|end
339			if let Some(a) = first.to_logical()
340				&& let Some(b) = third.to_logical()
341			{
342				return Ok(Self::Logical(a, lp1, b, fourth));
343			}
344			Err(Diagnostic::new(third.into(), Diagnostic::unexpected))?
345		} else {
346			// second is not LP — must be a keyword; first is the offset LP
347			// Pattern: <keyword> <keyword> <lp>  is invalid for four-value
348			Err(Diagnostic::new(second.into(), Diagnostic::unexpected))?
349		}
350	}
351}
352
353/// Horizontal axis keywords and `<length-percentage>`.
354///
355/// `left | center | right | x-start | x-end | <length-percentage>`
356#[derive(Parse, Peek, ToCursors, ToSpan, SemanticEq, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
357#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
358#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
359#[derive(csskit_derives::NodeWithMetadata)]
360pub enum PositionHorizontal {
361	#[atom(CssAtomSet::Left)]
362	Left(T![Ident]),
363	#[atom(CssAtomSet::Right)]
364	Right(T![Ident]),
365	#[atom(CssAtomSet::Center)]
366	Center(T![Ident]),
367	#[atom(CssAtomSet::XStart)]
368	XStart(T![Ident]),
369	#[atom(CssAtomSet::XEnd)]
370	XEnd(T![Ident]),
371	LengthPercentage(LengthPercentage),
372}
373
374/// Vertical axis keywords and `<length-percentage>`.
375///
376/// `top | center | bottom | y-start | y-end | <length-percentage>`
377#[derive(Parse, Peek, ToCursors, ToSpan, SemanticEq, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
378#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
379#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
380#[derive(csskit_derives::NodeWithMetadata)]
381pub enum PositionVertical {
382	#[atom(CssAtomSet::Top)]
383	Top(T![Ident]),
384	#[atom(CssAtomSet::Bottom)]
385	Bottom(T![Ident]),
386	#[atom(CssAtomSet::Center)]
387	Center(T![Ident]),
388	#[atom(CssAtomSet::YStart)]
389	YStart(T![Ident]),
390	#[atom(CssAtomSet::YEnd)]
391	YEnd(T![Ident]),
392	LengthPercentage(LengthPercentage),
393}
394
395/// Block axis keywords (flow-relative).
396///
397/// `block-start | block-end | center`
398#[derive(Parse, Peek, ToCursors, ToSpan, SemanticEq, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
399#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
400#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
401#[derive(csskit_derives::NodeWithMetadata)]
402pub enum PositionBlockAxis {
403	#[atom(CssAtomSet::BlockStart)]
404	BlockStart(T![Ident]),
405	#[atom(CssAtomSet::BlockEnd)]
406	BlockEnd(T![Ident]),
407	#[atom(CssAtomSet::Center)]
408	Center(T![Ident]),
409}
410
411/// Inline axis keywords (flow-relative).
412///
413/// `inline-start | inline-end | center`
414#[derive(Parse, Peek, ToCursors, ToSpan, SemanticEq, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
415#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
416#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
417#[derive(csskit_derives::NodeWithMetadata)]
418pub enum PositionInlineAxis {
419	#[atom(CssAtomSet::InlineStart)]
420	InlineStart(T![Ident]),
421	#[atom(CssAtomSet::InlineEnd)]
422	InlineEnd(T![Ident]),
423	#[atom(CssAtomSet::Center)]
424	Center(T![Ident]),
425}
426
427/// Axis-ambiguous logical keywords.
428///
429/// `start | end`
430///
431/// When used in a two-value position, the first represents the block axis and
432/// the second the inline axis.
433#[derive(Parse, Peek, ToCursors, ToSpan, SemanticEq, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
434#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
435#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
436#[derive(csskit_derives::NodeWithMetadata)]
437pub enum PositionLogical {
438	#[atom(CssAtomSet::Start)]
439	Start(T![Ident]),
440	#[atom(CssAtomSet::End)]
441	End(T![Ident]),
442}
443
444/// Horizontal edge keywords without `<length-percentage>` (for four-value syntax).
445///
446/// `left | right | x-start | x-end`
447#[derive(Parse, Peek, ToCursors, ToSpan, SemanticEq, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
448#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
449#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
450#[derive(csskit_derives::NodeWithMetadata)]
451pub enum PositionHorizontalKeyword {
452	#[atom(CssAtomSet::Left)]
453	Left(T![Ident]),
454	#[atom(CssAtomSet::Right)]
455	Right(T![Ident]),
456	#[atom(CssAtomSet::XStart)]
457	XStart(T![Ident]),
458	#[atom(CssAtomSet::XEnd)]
459	XEnd(T![Ident]),
460}
461
462/// Vertical edge keywords without `<length-percentage>` (for four-value syntax).
463///
464/// `top | bottom | y-start | y-end`
465#[derive(Parse, Peek, ToCursors, ToSpan, SemanticEq, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
466#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
467#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
468#[derive(csskit_derives::NodeWithMetadata)]
469pub enum PositionVerticalKeyword {
470	#[atom(CssAtomSet::Top)]
471	Top(T![Ident]),
472	#[atom(CssAtomSet::Bottom)]
473	Bottom(T![Ident]),
474	#[atom(CssAtomSet::YStart)]
475	YStart(T![Ident]),
476	#[atom(CssAtomSet::YEnd)]
477	YEnd(T![Ident]),
478}
479
480/// Block axis edge keywords without `center` (for four-value syntax).
481///
482/// `block-start | block-end`
483#[derive(Parse, Peek, ToCursors, ToSpan, SemanticEq, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
484#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
485#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
486#[derive(csskit_derives::NodeWithMetadata)]
487pub enum PositionBlockAxisKeyword {
488	#[atom(CssAtomSet::BlockStart)]
489	BlockStart(T![Ident]),
490	#[atom(CssAtomSet::BlockEnd)]
491	BlockEnd(T![Ident]),
492}
493
494/// Inline axis edge keywords without `center` (for four-value syntax).
495///
496/// `inline-start | inline-end`
497#[derive(Parse, Peek, ToCursors, ToSpan, SemanticEq, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
498#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
499#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
500#[derive(csskit_derives::NodeWithMetadata)]
501pub enum PositionInlineAxisKeyword {
502	#[atom(CssAtomSet::InlineStart)]
503	InlineStart(T![Ident]),
504	#[atom(CssAtomSet::InlineEnd)]
505	InlineEnd(T![Ident]),
506}
507
508#[cfg(test)]
509mod tests {
510	use super::*;
511	use crate::CssAtomSet;
512	use css_parse::{assert_parse, assert_parse_error, assert_parse_span};
513
514	#[test]
515	fn size_test() {
516		assert_eq!(std::mem::size_of::<Position>(), 68);
517	}
518
519	#[test]
520	fn test_writes() {
521		// One-value
522		assert_parse!(CssAtomSet::ATOMS, Position, "left", Position::One(PositionOne::Left(_)));
523		assert_parse!(CssAtomSet::ATOMS, Position, "right", Position::One(PositionOne::Right(_)));
524		assert_parse!(CssAtomSet::ATOMS, Position, "top", Position::One(PositionOne::Top(_)));
525		assert_parse!(CssAtomSet::ATOMS, Position, "bottom", Position::One(PositionOne::Bottom(_)));
526		assert_parse!(CssAtomSet::ATOMS, Position, "center", Position::One(PositionOne::Center(_)));
527		assert_parse!(CssAtomSet::ATOMS, Position, "x-start", Position::One(PositionOne::XStart(_)));
528		assert_parse!(CssAtomSet::ATOMS, Position, "x-end", Position::One(PositionOne::XEnd(_)));
529		assert_parse!(CssAtomSet::ATOMS, Position, "y-start", Position::One(PositionOne::YStart(_)));
530		assert_parse!(CssAtomSet::ATOMS, Position, "y-end", Position::One(PositionOne::YEnd(_)));
531		assert_parse!(CssAtomSet::ATOMS, Position, "block-start", Position::One(PositionOne::BlockStart(_)));
532		assert_parse!(CssAtomSet::ATOMS, Position, "block-end", Position::One(PositionOne::BlockEnd(_)));
533		assert_parse!(CssAtomSet::ATOMS, Position, "inline-start", Position::One(PositionOne::InlineStart(_)));
534		assert_parse!(CssAtomSet::ATOMS, Position, "inline-end", Position::One(PositionOne::InlineEnd(_)));
535		assert_parse!(CssAtomSet::ATOMS, Position, "start", Position::One(PositionOne::Start(_)));
536		assert_parse!(CssAtomSet::ATOMS, Position, "end", Position::One(PositionOne::End(_)));
537		assert_parse!(
538			CssAtomSet::ATOMS,
539			Position,
540			"50%",
541			Position::One(PositionOne::LengthPercentage(LengthPercentage::Percent(_)))
542		);
543		// Two-value physical
544		assert_parse!(
545			CssAtomSet::ATOMS,
546			Position,
547			"center center",
548			Position::Two(PositionTwo::Physical(PositionHorizontal::Center(_), PositionVertical::Center(_)))
549		);
550		assert_parse!(
551			CssAtomSet::ATOMS,
552			Position,
553			"center top",
554			Position::Two(PositionTwo::Physical(PositionHorizontal::Center(_), PositionVertical::Top(_)))
555		);
556		assert_parse!(
557			CssAtomSet::ATOMS,
558			Position,
559			"50% 50%",
560			Position::Two(PositionTwo::Physical(
561				PositionHorizontal::LengthPercentage(LengthPercentage::Percent(_)),
562				PositionVertical::LengthPercentage(LengthPercentage::Percent(_))
563			))
564		);
565		assert_parse!(
566			CssAtomSet::ATOMS,
567			Position,
568			"20px 30px",
569			Position::Two(PositionTwo::Physical(
570				PositionHorizontal::LengthPercentage(_),
571				PositionVertical::LengthPercentage(_)
572			))
573		);
574		assert_parse!(
575			CssAtomSet::ATOMS,
576			Position,
577			"2% bottom",
578			Position::Two(PositionTwo::Physical(
579				PositionHorizontal::LengthPercentage(LengthPercentage::Percent(_)),
580				PositionVertical::Bottom(_)
581			))
582		);
583		assert_parse!(
584			CssAtomSet::ATOMS,
585			Position,
586			"-70% -180%",
587			Position::Two(PositionTwo::Physical(
588				PositionHorizontal::LengthPercentage(LengthPercentage::Percent(_)),
589				PositionVertical::LengthPercentage(LengthPercentage::Percent(_))
590			))
591		);
592		assert_parse!(
593			CssAtomSet::ATOMS,
594			Position,
595			"right 8.5%",
596			Position::Two(PositionTwo::Physical(
597				PositionHorizontal::Right(_),
598				PositionVertical::LengthPercentage(LengthPercentage::Percent(_))
599			))
600		);
601		// Two-value physical with new keywords
602		assert_parse!(
603			CssAtomSet::ATOMS,
604			Position,
605			"x-start y-end",
606			Position::Two(PositionTwo::Physical(PositionHorizontal::XStart(_), PositionVertical::YEnd(_)))
607		);
608		// Two-value flow-relative
609		assert_parse!(
610			CssAtomSet::ATOMS,
611			Position,
612			"block-start inline-end",
613			Position::Two(PositionTwo::FlowRelative(
614				PositionBlockAxis::BlockStart(_),
615				PositionInlineAxis::InlineEnd(_)
616			))
617		);
618		assert_parse!(
619			CssAtomSet::ATOMS,
620			Position,
621			"inline-end block-start",
622			Position::Two(PositionTwo::FlowRelative(
623				PositionBlockAxis::BlockStart(_),
624				PositionInlineAxis::InlineEnd(_)
625			))
626		);
627		// Two-value logical
628		assert_parse!(
629			CssAtomSet::ATOMS,
630			Position,
631			"start end",
632			Position::Two(PositionTwo::Logical(PositionLogical::Start(_), PositionLogical::End(_)))
633		);
634		// Four-value physical
635		assert_parse!(
636			CssAtomSet::ATOMS,
637			Position,
638			"right -6px bottom 12vmin",
639			Position::Four(PositionFour::Physical(
640				PositionHorizontalKeyword::Right(_),
641				LengthPercentage::Length(_),
642				PositionVerticalKeyword::Bottom(_),
643				LengthPercentage::Length(_)
644			))
645		);
646		assert_parse!(
647			CssAtomSet::ATOMS,
648			Position,
649			"bottom 12vmin right -6px",
650			Position::Four(PositionFour::Physical(
651				PositionHorizontalKeyword::Right(_),
652				LengthPercentage::Length(_),
653				PositionVerticalKeyword::Bottom(_),
654				LengthPercentage::Length(_)
655			))
656		);
657		// Four-value flow-relative
658		assert_parse!(
659			CssAtomSet::ATOMS,
660			Position,
661			"block-start 10px inline-end 20px",
662			Position::Four(PositionFour::FlowRelative(
663				PositionBlockAxisKeyword::BlockStart(_),
664				LengthPercentage::Length(_),
665				PositionInlineAxisKeyword::InlineEnd(_),
666				LengthPercentage::Length(_)
667			))
668		);
669		// Four-value logical
670		assert_parse!(
671			CssAtomSet::ATOMS,
672			Position,
673			"start 10px end 20px",
674			Position::Four(PositionFour::Logical(
675				PositionLogical::Start(_),
676				LengthPercentage::Length(_),
677				PositionLogical::End(_),
678				LengthPercentage::Length(_)
679			))
680		);
681	}
682
683	#[test]
684	fn test_errors() {
685		assert_parse_error!(CssAtomSet::ATOMS, Position, "left left");
686		assert_parse_error!(CssAtomSet::ATOMS, Position, "bottom top");
687		assert_parse_error!(CssAtomSet::ATOMS, Position, "10px 15px 20px 15px");
688		// 3 value syntax is not allowed
689		assert_parse_error!(CssAtomSet::ATOMS, Position, "right -6px bottom");
690	}
691
692	#[test]
693	fn test_spans() {
694		// Parsing should stop at var()
695		assert_parse_span!(
696			CssAtomSet::ATOMS,
697			Position,
698			r#"
699			right var(--foo)
700			^^^^^
701		"#
702		);
703		// Parsing should stop at four values:
704		assert_parse_span!(
705			CssAtomSet::ATOMS,
706			Position,
707			r#"
708			right -6px bottom 12rem 8px 20%
709			^^^^^^^^^^^^^^^^^^^^^^^
710		"#
711		);
712	}
713}