css_rs_macro/lib.rs
1//! css-rs-macro is a procedural macro that allows you to write your CSS next to your Rust views.
2//!
3//! github.com/chinedufn/percy/examples/css-in-rust
4
5#[feature(proc_macro)]
6#[macro_use]
7extern crate syn;
8#[macro_use]
9extern crate quote;
10#[macro_use]
11extern crate lazy_static;
12extern crate proc_macro;
13
14use syn::Expr;
15
16use proc_macro::TokenStream;
17use proc_macro::TokenTree;
18
19use std::env;
20use std::fs;
21use std::fs::File;
22use std::fs::OpenOptions;
23use std::io::Write;
24use std::path::Path;
25use std::sync::Mutex;
26
27lazy_static! {
28 static ref CSS_COUNTER: Mutex<u32> = Mutex::new(0);
29}
30
31/// Parses the syntax for writing inline css. Every call to css! will have its class
32/// name incremented by one.
33///
34/// So your first css! call is class "._css_rs_0", then "._css_rs_1", etc.
35///
36/// To write your css to a file use:
37///
38/// ```ignore
39/// OUTPUT_CSS=/path/to/my/output.css cargo run my-app
40/// ```
41///
42/// # Examples
43///
44/// ```ignore
45/// #![feature(use_extern_macros)]
46/// #![feature(proc_macro_non_items)]
47///
48/// extern crate css_rs_macro;
49/// use css_rs_macro::css;
50///
51/// fn main () {
52/// let class1 = css! {
53/// "
54/// :host {
55/// background-color: red;
56/// }
57///
58/// :host > div {
59/// display: flex;
60/// align-items: center;
61/// }
62/// "
63/// };
64///
65/// let class2 = css! {r#"
66/// :host { display: flex; }
67/// "#};
68///
69/// assert_eq!(class1, "_css_rs_0".to_string());
70/// assert_eq!(class2, "_css_rs_1".to_string());
71/// }
72/// ```
73#[proc_macro]
74pub fn css(input: TokenStream) -> TokenStream {
75 let mut css_counter = CSS_COUNTER.lock().unwrap();
76
77 let class = format!("_css_rs_{}", css_counter);
78
79 let css_file = env::vars().find(|(key, _)| key == "OUTPUT_CSS");
80
81 if css_file.is_some() {
82 let css_file = css_file.unwrap().1;
83
84 if *css_counter == 0 {
85 if Path::new(&css_file).exists() {
86 fs::remove_file(&css_file);
87 }
88
89 let mut css_file = OpenOptions::new()
90 .write(true)
91 .create_new(true)
92 .open(css_file)
93 .unwrap();
94
95 write_css_to_file(&mut css_file, &class, input);
96 } else {
97 let mut css_file = OpenOptions::new().append(true).open(css_file).unwrap();
98
99 write_css_to_file(&mut css_file, &class, input);
100 }
101 }
102
103 *css_counter += 1;
104
105 let expanded = quote! {
106 #class
107 };
108
109 expanded.into()
110}
111
112fn write_css_to_file(css_file: &mut File, class: &str, input: TokenStream) {
113 for css in input.into_iter() {
114 let mut css = css.to_string();
115
116 // Remove the surrounding quotes from so that we can write only the
117 // CSS to our file.
118 //
119 // Handles:
120 // css!{r#" :host { ... } "#}
121 // as well as
122 // css!{" :host { ... } "}
123 let first_quote_mark = css.find(r#"""#).unwrap();
124 let last_quote_mark = css.rfind(r#"""#).unwrap();
125 css.truncate(last_quote_mark);
126 let mut css = css.split_off(first_quote_mark + 1);
127
128 // Replace :host selectors with the class name of the :host element
129 // A fake shadow-dom implementation.. if you will..
130 let css = css.replace(":host", &format!(".{}", class));
131
132 css_file.write(&css.into_bytes()).unwrap();
133 css_file.write("\n".as_bytes()).unwrap();
134 }
135}