1use proc_macro2::{Span, TokenStream};
23use syn::ext::IdentExt;
24use syn::parse::{Parse, ParseStream};
25use syn::spanned::Spanned;
26use syn::{Ident, LitFloat, LitInt, LitStr, Token, braced, parenthesized};
27
28#[derive(Clone, Debug)]
29pub struct StyleItem {
30 pub motion: Vec<MotionDecl>,
31 pub transitions: Vec<MotionDecl>,
33 pub patterns: Vec<MotionDecl>,
35 pub accents: Vec<MotionDecl>,
38 pub text: Vec<MotionDecl>,
40 pub corners: Vec<CornerBlock>,
43 pub rules: Vec<StyleRuleItem>,
44 pub span: Span,
45}
46
47#[derive(Clone, Debug)]
49pub struct MotionDecl {
50 pub name: Ident,
51 pub value: StyleValue,
52}
53
54#[derive(Clone, Debug)]
57pub struct CornerBlock {
58 pub targets: Vec<Ident>,
59 pub corners: Vec<MotionDecl>,
60 pub span: Span,
61}
62
63#[derive(Clone, Debug)]
65pub struct StyleRuleItem {
66 pub component: Ident,
67 pub variant: Option<Ident>,
68 pub targets: Vec<Ident>,
71 pub exempt: Vec<(Ident, String)>,
74 pub props: Vec<StyleProp>,
75 pub span: Span,
76}
77
78#[derive(Clone, Debug)]
79pub struct StyleProp {
80 pub name: Ident,
81 pub value: StyleValue,
82}
83
84#[derive(Clone, Debug)]
87pub enum StyleValue {
88 Number(f64, Span),
89 Name(Vec<Ident>),
90 Text(String, Span),
91 Call { name: Ident, args: Vec<StyleArg> },
92 Sum(Vec<StyleValue>),
93}
94
95#[derive(Clone, Debug)]
96pub struct StyleArg {
97 pub name: Option<Ident>,
98 pub value: StyleValue,
99}
100
101impl StyleValue {
102 pub fn span(&self) -> Span {
103 match self {
104 StyleValue::Number(_, span) | StyleValue::Text(_, span) => *span,
105 StyleValue::Name(path) => path[0].span(),
106 StyleValue::Call { name, .. } => name.span(),
107 StyleValue::Sum(parts) => parts[0].span(),
108 }
109 }
110
111 pub fn dotted(&self) -> Option<String> {
113 match self {
114 StyleValue::Name(path) => Some(
115 path.iter()
116 .map(ToString::to_string)
117 .collect::<Vec<_>>()
118 .join("."),
119 ),
120 _ => None,
121 }
122 }
123
124 pub fn terms(&self) -> Vec<&StyleValue> {
126 match self {
127 StyleValue::Sum(parts) => parts.iter().collect(),
128 other => vec![other],
129 }
130 }
131}
132
133impl Parse for StyleItem {
134 fn parse(input: ParseStream) -> syn::Result<Self> {
135 let span = input.span();
136 let mut motion = Vec::new();
137 let mut transitions = Vec::new();
138 let mut patterns = Vec::new();
139 let mut accents = Vec::new();
140 let mut text = Vec::new();
141 let mut corners = Vec::new();
142 let mut rules = Vec::new();
143 while !input.is_empty() {
144 let attrs = input.call(syn::Attribute::parse_outer)?;
145 let component: Ident = input.parse()?;
146 if component == "corner" && input.peek(syn::token::Brace) {
147 let mut targets = Vec::new();
148 for attr in &attrs {
149 if !attr.path().is_ident("target") {
150 return Err(syn::Error::new(
151 attr.span(),
152 "a `corner` block takes only `#[target(…)]`",
153 ));
154 }
155 let list = attr.parse_args_with(
156 syn::punctuated::Punctuated::<Ident, Token![,]>::parse_terminated,
157 )?;
158 targets.extend(list);
159 }
160 let content;
161 braced!(content in input);
162 let mut list = Vec::new();
163 while !content.is_empty() {
164 let name: Ident = content.parse()?;
165 content.parse::<Token![:]>()?;
166 let value: StyleValue = content.parse()?;
167 list.push(MotionDecl { name, value });
168 if !content.is_empty() {
169 content.parse::<Token![,]>()?;
170 }
171 }
172 corners.push(CornerBlock {
173 targets,
174 corners: list,
175 span: component.span(),
176 });
177 continue;
178 }
179 if (component == "motion"
180 || component == "transition"
181 || component == "pattern"
182 || component == "accents"
183 || component == "text")
184 && attrs.is_empty()
185 && input.peek(syn::token::Brace)
186 {
187 let content;
188 braced!(content in input);
189 let list = if component == "motion" {
190 &mut motion
191 } else if component == "transition" {
192 &mut transitions
193 } else if component == "accents" {
194 &mut accents
195 } else if component == "text" {
196 &mut text
197 } else {
198 &mut patterns
199 };
200 while !content.is_empty() {
201 let name: Ident = content.parse()?;
202 content.parse::<Token![:]>()?;
203 let value: StyleValue = content.parse()?;
204 list.push(MotionDecl { name, value });
205 if !content.is_empty() {
206 content.parse::<Token![,]>()?;
207 }
208 }
209 continue;
210 }
211 let variant = if input.peek(Token![.]) {
212 input.parse::<Token![.]>()?;
213 Some(input.parse()?)
214 } else {
215 None
216 };
217 let mut targets = Vec::new();
218 let mut exempt = Vec::new();
219 for attr in &attrs {
220 if attr.path().is_ident("target") {
221 let list = attr.parse_args_with(
222 syn::punctuated::Punctuated::<Ident, Token![,]>::parse_terminated,
223 )?;
224 targets.extend(list);
225 } else if attr.path().is_ident("exempt") {
226 attr.parse_args_with(|input: ParseStream| {
227 let target: Ident = input.parse()?;
228 input.parse::<Token![,]>()?;
229 let reason: LitStr = input.parse()?;
230 exempt.push((target, reason.value()));
231 Ok(())
232 })?;
233 } else {
234 return Err(syn::Error::new(
235 attr.span(),
236 "style rules take `#[target(…)]` and `#[exempt(target, \"reason\")]`",
237 ));
238 }
239 }
240 let content;
241 braced!(content in input);
242 let mut props = Vec::new();
243 while !content.is_empty() {
244 let name: Ident = content.parse()?;
245 content.parse::<Token![:]>()?;
246 let value: StyleValue = content.parse()?;
247 props.push(StyleProp { name, value });
248 if !content.is_empty() {
249 content.parse::<Token![,]>()?;
250 }
251 }
252 rules.push(StyleRuleItem {
253 span: component.span(),
254 component,
255 variant,
256 targets,
257 exempt,
258 props,
259 });
260 }
261 Ok(StyleItem {
262 motion,
263 transitions,
264 patterns,
265 accents,
266 text,
267 corners,
268 rules,
269 span,
270 })
271 }
272}
273
274impl Parse for StyleValue {
275 fn parse(input: ParseStream) -> syn::Result<Self> {
276 let mut parts = vec![term(input)?];
277 while input.peek(Token![+]) {
278 input.parse::<Token![+]>()?;
279 parts.push(term(input)?);
280 }
281 Ok(if parts.len() == 1 {
282 parts.pop().unwrap()
283 } else {
284 StyleValue::Sum(parts)
285 })
286 }
287}
288
289fn number(input: ParseStream, negative: bool) -> syn::Result<StyleValue> {
290 let sign = if negative { -1.0 } else { 1.0 };
291 if input.peek(LitFloat) {
292 let lit: LitFloat = input.parse()?;
293 Ok(StyleValue::Number(
294 sign * crate::finite(lit.base10_parse()?, lit.span())?,
295 lit.span(),
296 ))
297 } else {
298 let lit: LitInt = input.parse()?;
299 Ok(StyleValue::Number(
300 sign * crate::finite(lit.base10_parse()?, lit.span())?,
301 lit.span(),
302 ))
303 }
304}
305
306fn term(input: ParseStream) -> syn::Result<StyleValue> {
307 if input.peek(Token![-]) {
308 input.parse::<Token![-]>()?;
309 return number(input, true);
310 }
311 if input.peek(LitFloat) || input.peek(LitInt) {
312 return number(input, false);
313 }
314 if input.peek(LitStr) {
315 let lit: LitStr = input.parse()?;
316 return Ok(StyleValue::Text(lit.value(), lit.span()));
317 }
318 let name: Ident = input.call(Ident::parse_any)?;
320 if input.peek(syn::token::Paren) {
321 let content;
322 parenthesized!(content in input);
323 let mut args = Vec::new();
324 while !content.is_empty() {
325 let named = content.peek(Ident) && content.peek2(Token![:]);
326 let name = if named {
327 let name: Ident = content.parse()?;
328 content.parse::<Token![:]>()?;
329 Some(name)
330 } else {
331 None
332 };
333 let value: StyleValue = content.parse()?;
334 args.push(StyleArg { name, value });
335 if !content.is_empty() {
336 content.parse::<Token![,]>()?;
337 }
338 }
339 return Ok(StyleValue::Call { name, args });
340 }
341 let mut path = vec![name];
342 while input.peek(Token![.]) {
343 input.parse::<Token![.]>()?;
344 path.push(input.parse()?);
345 }
346 Ok(StyleValue::Name(path))
347}
348
349pub fn parse_style(tokens: TokenStream) -> syn::Result<StyleItem> {
351 syn::parse2(tokens)
352}
353
354#[cfg(test)]
355mod tests {
356 use super::*;
357
358 #[test]
359 fn a_style_reads_rules_motion_and_attributes() {
360 let style = parse_style(quote::quote! {
361 motion {
362 jelly: spring(period: 0.42, damping: 0.42),
363 }
364 SidebarItem {
365 corner: 8,
366 hover: scale(1.03, snappy),
367 selected: fill(accent),
368 }
369 IconButton {
370 hover: background(fill.subtle) + scale(1.08, snappy),
371 }
372 #[target(macos)]
373 #[exempt(gtk, "no transforms on rows")]
374 Button.hero { size: large, pressed: scale(x: 0.97, y: 0.9, press) }
375 })
376 .unwrap();
377 assert_eq!(style.motion.len(), 1);
378 assert_eq!(style.motion[0].name, "jelly");
379 assert_eq!(style.rules.len(), 3);
380 let icon = &style.rules[1];
381 let StyleValue::Sum(parts) = &icon.props[0].value else {
382 panic!("a sum")
383 };
384 assert_eq!(parts.len(), 2);
385 assert_eq!(parts[0].clone().terms().len(), 1);
386 let hero = &style.rules[2];
387 assert_eq!(hero.variant.as_ref().unwrap(), "hero");
388 assert_eq!(hero.targets[0], "macos");
389 assert_eq!(hero.exempt[0].1, "no transforms on rows");
390 let StyleValue::Call { args, .. } = &hero.props[1].value else {
391 panic!("a call")
392 };
393 assert_eq!(args[0].name.as_ref().unwrap(), "x");
394 assert!(matches!(args[2].value, StyleValue::Name(_)));
395 assert!(parse_style(quote::quote! { Button { corner } }).is_err());
396 }
397
398 #[test]
399 fn numbers_out_of_range_for_f64_are_rejected() {
400 for tokens in [
401 quote::quote! { Button { corner: 1e999 } },
402 quote::quote! { Button { hover: scale(-1e999, snappy) } },
403 ] {
404 let error = parse_style(tokens).unwrap_err().to_string();
405 assert!(error.contains("out of range for f64"), "{error}");
406 }
407 }
408}