percy_css_macro/
lib.rs

1//! percy-css-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/// #[macro_use]
46/// extern crate percy_css_macro;
47///
48/// fn main () {
49///     let class1 = css! {
50///       "
51///       :host {
52///         background-color: red;
53///       }
54///
55///       :host > div {
56///         display: flex;
57///         align-items: center;
58///       }
59///       "
60///     };
61///
62///     let class2 = css! {r#"
63///         :host { display: flex; }
64///     "#};
65///
66///     assert_eq!(class1, "_css_rs_0".to_string());
67///     assert_eq!(class2, "_css_rs_1".to_string());
68/// }
69/// ```
70#[proc_macro]
71pub fn css(input: TokenStream) -> TokenStream {
72    let mut css_counter = CSS_COUNTER.lock().unwrap();
73
74    let class = format!("_css_rs_{}", css_counter);
75
76    let css_file = env::vars().find(|(key, _)| key == "OUTPUT_CSS");
77
78    if css_file.is_some() {
79        let css_file = css_file.unwrap().1;
80
81        if *css_counter == 0 {
82            if Path::new(&css_file).exists() {
83                fs::remove_file(&css_file).unwrap();
84            }
85
86            let mut css_file = OpenOptions::new()
87                .write(true)
88                .create_new(true)
89                .open(css_file)
90                .unwrap();
91
92            write_css_to_file(&mut css_file, &class, input);
93        } else {
94            let mut css_file = OpenOptions::new().append(true).open(css_file).unwrap();
95
96            write_css_to_file(&mut css_file, &class, input);
97        }
98    }
99
100    *css_counter += 1;
101
102    let expanded = quote! {
103    #class
104    };
105
106    expanded.into()
107}
108
109fn write_css_to_file(css_file: &mut File, class: &str, input: TokenStream) {
110    for css in input.into_iter() {
111        let mut css = css.to_string();
112
113        // Remove the surrounding quotes from so that we can write only the
114        // CSS to our file.
115        //
116        // Handles:
117        //   css!{r#" :host { ... } "#}
118        //     as well as
119        //   css!{" :host { ... } "}
120        let first_quote_mark = css.find(r#"""#).unwrap();
121        let last_quote_mark = css.rfind(r#"""#).unwrap();
122        css.truncate(last_quote_mark);
123        let mut css = css.split_off(first_quote_mark + 1);
124
125        // Replace :host selectors with the class name of the :host element
126        // A fake shadow-dom implementation.. if you will..
127        let css = css.replace(":host", &format!(".{}", class));
128
129        css_file.write(&css.into_bytes()).unwrap();
130        css_file.write("\n".as_bytes()).unwrap();
131    }
132}