Skip to main content

css_ast/types/
bg_position.rs

1use super::prelude::*;
2use crate::{
3	CalcableValue, LengthPercentage, PositionFour, PositionHorizontal, PositionHorizontalKeyword, PositionOne,
4	PositionTwo, PositionVertical, PositionVerticalKeyword,
5};
6
7/// <https://drafts.csswg.org/css-backgrounds-4/#typedef-bg-position>
8///
9/// ```text,ignore
10/// <bg-position> = <position> | <position-three>
11/// <position-three> = [
12///   [ left | center | right ] && [ [ top | bottom ] <length-percentage> ]
13/// |
14///   [ [ left | right ] <length-percentage> ] && [ top | center | bottom ]
15/// ]
16/// ```
17///
18/// Extends `<position>` with 3-value forms only valid in `background-position`.
19#[node]
20#[derive(ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
21#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
22#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
23#[derive(csskit_derives::NodeWithMetadata)]
24pub enum BgPosition<'a> {
25	/// Single value syntax
26	One(PositionOne<'a>),
27	/// Two value syntax
28	Two(PositionTwo<'a>),
29	/// Three value syntax where the Horizontal axis may be "center"
30	ThreeHorizontal(PositionHorizontal<'a>, PositionVerticalKeyword, CalcableValue<'a, LengthPercentage>),
31	/// Three value syntax where the Vertical axis may be "center"
32	ThreeVertical(PositionHorizontalKeyword, CalcableValue<'a, LengthPercentage>, PositionVertical<'a>),
33	/// Four value syntax
34	Four(PositionFour<'a>),
35}
36
37impl<'a> Peek<'a> for BgPosition<'a> {
38	const PEEK_KINDSET: KindSet = PositionOne::PEEK_KINDSET;
39
40	#[inline(always)]
41	fn peek<I>(p: &Parser<'a, I>, c: Cursor) -> bool
42	where
43		I: Iterator<Item = Cursor> + Clone,
44	{
45		PositionOne::peek(p, c)
46	}
47}
48
49impl<'a> Parse<'a> for BgPosition<'a> {
50	fn parse<I>(p: &mut Parser<'a, I>) -> ParserResult<Self>
51	where
52		I: Iterator<Item = Cursor> + Clone,
53	{
54		let first = p.parse::<PositionOne>()?;
55		if !p.peek::<PositionOne>() {
56			return Ok(Self::One(first));
57		}
58
59		let second_c = p.peek_n(1);
60		let second = p.parse::<PositionOne>()?;
61		if !p.peek::<PositionOne>() {
62			return Ok(Self::Two(PositionTwo::from_two(p, first, second, second_c)?));
63		}
64
65		if let Some(h_kw) = first.to_horizontal_keyword() {
66			if let PositionOne::LengthPercentage(lp) = &second {
67				// `H L ? ...` — three-value or four-value
68				let third_c = p.peek_n(1);
69				let third = p.parse::<PositionOne>()?;
70				if let Some(v) = third.to_vertical() {
71					let fourth = if third.to_vertical_keyword().is_some() {
72						p.parse_if_peek::<CalcableValue<LengthPercentage>>()?
73					} else {
74						None
75					};
76					match (fourth, third.to_vertical_keyword()) {
77						// Four-value physical: `left 10px bottom 20px`
78						(Some(fourth), Some(v_kw)) => {
79							return Ok(Self::Four(PositionFour::Physical(h_kw, lp.clone(), v_kw, fourth)));
80						}
81						// Three-value: `left 10px center`
82						(None, _) => return Ok(Self::ThreeVertical(h_kw, lp.clone(), v)),
83						_ => {}
84					}
85				}
86				Err(Diagnostic::new(third_c, Diagnostic::unexpected))?
87			} else if let Some(v_kw) = second.to_vertical_keyword() {
88				// `H V L` — three-value: `left bottom 10px`
89				let third = p.parse::<CalcableValue<LengthPercentage>>()?;
90				return Ok(Self::ThreeHorizontal(PositionHorizontal::from_keyword(h_kw), v_kw, third));
91			}
92			Err(Diagnostic::new(second_c, Diagnostic::unexpected))?
93		} else if let PositionOne::Center(t) = first {
94			if let Some(v_kw) = second.to_vertical_keyword() {
95				// Three-value: `center bottom 10px`
96				let third = p.parse::<CalcableValue<LengthPercentage>>()?;
97				return Ok(Self::ThreeHorizontal(PositionHorizontal::Center(t), v_kw, third));
98			} else if let Some(v) = second.to_vertical() {
99				return Ok(Self::Two(PositionTwo::Physical(PositionHorizontal::Center(t), v)));
100			}
101			Err(Diagnostic::new(second_c, Diagnostic::unexpected))?
102		} else {
103			// Vertical keyword first — could be reversed four-value handled by Position::Four
104			// e.g. `bottom 20px left 10px`
105			Ok(Self::Four(PositionFour::from_four(p, first, second, second_c)?))
106		}
107	}
108}
109
110impl<'a> PositionHorizontal<'a> {
111	pub(crate) fn from_keyword(kw: PositionHorizontalKeyword) -> Self {
112		match kw {
113			PositionHorizontalKeyword::Left(t) => Self::Left(t),
114			PositionHorizontalKeyword::Right(t) => Self::Right(t),
115			PositionHorizontalKeyword::XStart(t) => Self::XStart(t),
116			PositionHorizontalKeyword::XEnd(t) => Self::XEnd(t),
117		}
118	}
119}
120
121#[cfg(test)]
122mod tests {
123	use super::*;
124	use crate::CssAtomSet;
125	use css_parse::{assert_parse, assert_parse_error, assert_peek_false};
126
127	#[test]
128	fn test_writes() {
129		assert_parse!(CssAtomSet::ATOMS, BgPosition, "center");
130		assert_parse!(CssAtomSet::ATOMS, BgPosition, "left");
131		assert_parse!(CssAtomSet::ATOMS, BgPosition, "50%");
132		assert_parse!(CssAtomSet::ATOMS, BgPosition, "center center");
133		assert_parse!(CssAtomSet::ATOMS, BgPosition, "left top");
134		assert_parse!(CssAtomSet::ATOMS, BgPosition, "0 0");
135		assert_parse!(CssAtomSet::ATOMS, BgPosition, "-80px 0");
136		assert_parse!(CssAtomSet::ATOMS, BgPosition, "right 8px bottom 20px");
137		assert_parse!(CssAtomSet::ATOMS, BgPosition, "left 10px top");
138		assert_parse!(CssAtomSet::ATOMS, BgPosition, "left 10px bottom");
139		assert_parse!(CssAtomSet::ATOMS, BgPosition, "right 10px center");
140		assert_parse!(CssAtomSet::ATOMS, BgPosition, "center bottom 10px");
141		assert_parse!(CssAtomSet::ATOMS, BgPosition, "left bottom 10px");
142	}
143
144	#[test]
145	fn test_errors() {
146		assert_peek_false!(CssAtomSet::ATOMS, BgPosition, "");
147		assert_parse_error!(CssAtomSet::ATOMS, BgPosition, "left left");
148		assert_parse_error!(CssAtomSet::ATOMS, BgPosition, "top top");
149	}
150}