css_loader_macros/
lib.rs

1//#![feature(proc_macro_span)]
2
3use cssparser::{ParseError, Parser, ParserInput, ToCss, Token};
4use proc_macro::TokenStream;
5use proc_macro2::{Ident, Span};
6use quote::{format_ident, quote};
7use std::error::Error;
8use std::fmt::Write;
9use std::fs;
10use std::path::{Path, PathBuf};
11use syn::LitStr;
12
13fn get_base_name(filename: &Path, full_path: bool) -> Result<String, Box<dyn Error>> {
14    let out = if full_path {
15        filename
16            .parent()
17            .ok_or("Can't get parent name")?
18            .join(filename.file_stem().ok_or("Can't get file stem")?)
19            .to_str()
20            .ok_or("Can.t convert path to string")?
21            .to_owned()
22            .replace('/', "_")
23    } else {
24        filename
25            .file_stem()
26            .ok_or("Can't get file stem")?
27            .to_str()
28            .ok_or("Can't get file stem")?
29            .to_owned()
30    };
31
32    Ok(out)
33}
34
35struct ParsedCss {
36    generated_css: String,
37    idents: Vec<(String, String)>,
38}
39
40fn parse_css(filename: &Path, css_source: &[u8]) -> Result<ParsedCss, Box<dyn Error>> {
41    let css_source = String::from_utf8(css_source.into())?;
42    let mut input = ParserInput::new(&css_source);
43    let mut parser = Parser::new(&mut input);
44
45    let mut css_out = String::new();
46    let mut idents = Vec::new();
47
48    let mut is_class = true;
49    while !parser.is_exhausted() {
50        match parser.next().unwrap() {
51            Token::Ident(i) => {
52                if is_class {
53                    css_out += i;
54                } else {
55                    let tag = get_base_name(filename, true)?;
56                    let tagged_ident = format!("{}-{}", tag, i);
57
58                    write!(&mut css_out, ".{}", tagged_ident)?;
59                    idents.push((i.to_string(), tagged_ident));
60                }
61            }
62            Token::CurlyBracketBlock => {
63                parser
64                    .parse_nested_block(|parser| -> Result<(), ParseError<'_, &String>> {
65                        css_out += "{";
66                        while !parser.is_exhausted() {
67                            css_out += &parser.next().unwrap().to_css_string();
68                        }
69                        css_out += "}";
70                        Ok(())
71                    })
72                    .map_err(|_| "Can't parse CSS")?;
73                is_class = true;
74            }
75            Token::Delim('.') => {
76                is_class = false;
77            }
78            token => {
79                println!("{:?}", token);
80                css_out += &token.to_css_string();
81            }
82        }
83    }
84
85    Ok(ParsedCss {
86        idents,
87        generated_css: css_out,
88    })
89}
90
91fn compile_sass(source: &[u8]) -> Result<Vec<u8>, Box<dyn Error>> {
92    rsass::compile_scss(
93        source,
94        rsass::output::Format {
95            style: rsass::output::Style::Compressed,
96            ..Default::default()
97        },
98    )
99    .map_err(|e| format!("Can't parse SASS {}", e).into())
100}
101
102fn load_css(filename: &Path) -> Result<Vec<u8>, Box<dyn Error>> {
103    let is_sass = filename.extension().ok_or("Can't get extensions")? == "scss";
104    let manifest_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
105    let full_path = manifest_dir.join(filename);
106
107    let source = fs::read(full_path)?;
108
109    if is_sass {
110        compile_sass(&source)
111    } else {
112        Ok(source)
113    }
114}
115
116/// Reads the specified CSS or SASS file into the project, and then it scopes it to the local file.
117/// Paths are relative to CARGO_MANIFEST_DIR.
118#[proc_macro]
119pub fn import_style(item: TokenStream) -> TokenStream {
120    let filename: PathBuf = syn::parse::<LitStr>(item).unwrap().value().into();
121
122    let css = load_css(&filename).unwrap();
123    let parsed = parse_css(&filename, &css).unwrap();
124
125    let base_name = get_base_name(&filename, false).unwrap();
126    let inner_struct_name = format_ident!("__{}", base_name);
127
128    let inner_struct_fields = parsed.idents.iter().map(|(ident, _)| {
129        let ident = Ident::new(ident, Span::call_site());
130        quote! { #ident: &'static str, }
131    });
132
133    let outer_struct_name = format_ident!("_{}", base_name);
134
135    let inner_struct_values = parsed.idents.iter().map(|(ident, new_ident)| {
136        let ident = format_ident!("{}", ident);
137        quote! { #ident: #new_ident, }
138    });
139
140    let base_name_ident = format_ident!("{}", base_name);
141    let css = parsed.generated_css;
142    quote! {
143        use std::ops::Deref;
144
145        struct #inner_struct_name {
146            #(#inner_struct_fields)*
147        }
148
149        struct #outer_struct_name {
150            css: &'static str,
151            names: #inner_struct_name,
152            initialized: bool,
153        }
154
155        static #base_name_ident: #outer_struct_name = #outer_struct_name {
156            css: #css,
157            names: #inner_struct_name {
158                #(#inner_struct_values)*
159            },
160            initialized: false,
161        };
162
163        impl Deref for #outer_struct_name {
164            type Target = #inner_struct_name;
165
166            fn deref(&self) -> &Self::Target {
167                if !self.initialized {
168                    let init = &self.initialized as *const bool;
169                    let init_mut = init as *mut bool;
170                    unsafe {
171                        *init_mut = true;
172                    }
173
174                    let window = css_loader::web_sys::window().unwrap();
175                    let document = window.document().unwrap();
176                    let css = document.create_element("style").unwrap();
177                    css.set_text_content(Some(self.css));
178                    document.head().unwrap().append_child(&css).unwrap();
179                }
180
181                &self.names
182            }
183        }
184    }
185    .into()
186}