Skip to main content

css_ast/types/
position.rs

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