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