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}