use cssparser::{ParseError, Parser, ParserInput, ToCss, Token};
use proc_macro::TokenStream;
use proc_macro2::{Ident, Span};
use quote::{format_ident, quote};
use std::error::Error;
use std::fmt::Write;
use std::fs;
use std::path::{Path, PathBuf};
use syn::LitStr;
fn get_base_name(filename: &Path, full_path: bool) -> Result<String, Box<dyn Error>> {
let out = if full_path {
filename
.parent()
.ok_or("Can't get parent name")?
.join(filename.file_stem().ok_or("Can't get file stem")?)
.to_str()
.ok_or("Can.t convert path to string")?
.to_owned()
.replace('/', "_")
} else {
filename
.file_stem()
.ok_or("Can't get file stem")?
.to_str()
.ok_or("Can't get file stem")?
.to_owned()
};
Ok(out)
}
struct ParsedCss {
generated_css: String,
idents: Vec<(String, String)>,
}
fn parse_css(filename: &Path, css_source: &[u8]) -> Result<ParsedCss, Box<dyn Error>> {
let css_source = String::from_utf8(css_source.into())?;
let mut input = ParserInput::new(&css_source);
let mut parser = Parser::new(&mut input);
let mut css_out = String::new();
let mut idents = Vec::new();
let mut is_class = true;
while !parser.is_exhausted() {
match parser.next().unwrap() {
Token::Ident(i) => {
if is_class {
css_out += i;
} else {
let tag = get_base_name(filename, true)?;
let tagged_ident = format!("{}-{}", tag, i);
write!(&mut css_out, ".{}", tagged_ident)?;
idents.push((i.to_string(), tagged_ident));
}
}
Token::CurlyBracketBlock => {
parser
.parse_nested_block(|parser| -> Result<(), ParseError<'_, &String>> {
css_out += "{";
while !parser.is_exhausted() {
css_out += &parser.next().unwrap().to_css_string();
}
css_out += "}";
Ok(())
})
.map_err(|_| "Can't parse CSS")?;
is_class = true;
}
Token::Delim('.') => {
is_class = false;
}
token => {
println!("{:?}", token);
css_out += &token.to_css_string();
}
}
}
Ok(ParsedCss {
idents,
generated_css: css_out,
})
}
fn compile_sass(source: &[u8]) -> Result<Vec<u8>, Box<dyn Error>> {
rsass::compile_scss(
source,
rsass::output::Format {
style: rsass::output::Style::Compressed,
..Default::default()
},
)
.map_err(|e| format!("Can't parse SASS {}", e).into())
}
fn load_css(filename: &Path) -> Result<Vec<u8>, Box<dyn Error>> {
let is_sass = filename.extension().ok_or("Can't get extensions")? == "scss";
let manifest_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
let full_path = manifest_dir.join(filename);
let source = fs::read(full_path)?;
if is_sass {
compile_sass(&source)
} else {
Ok(source)
}
}
#[proc_macro]
pub fn import_style(item: TokenStream) -> TokenStream {
let filename: PathBuf = syn::parse::<LitStr>(item).unwrap().value().into();
let css = load_css(&filename).unwrap();
let parsed = parse_css(&filename, &css).unwrap();
let base_name = get_base_name(&filename, false).unwrap();
let inner_struct_name = format_ident!("__{}", base_name);
let inner_struct_fields = parsed.idents.iter().map(|(ident, _)| {
let ident = Ident::new(ident, Span::call_site());
quote! { #ident: &'static str, }
});
let outer_struct_name = format_ident!("_{}", base_name);
let inner_struct_values = parsed.idents.iter().map(|(ident, new_ident)| {
let ident = format_ident!("{}", ident);
quote! { #ident: #new_ident, }
});
let base_name_ident = format_ident!("{}", base_name);
let css = parsed.generated_css;
quote! {
use std::ops::Deref;
struct #inner_struct_name {
#(#inner_struct_fields)*
}
struct #outer_struct_name {
css: &'static str,
names: #inner_struct_name,
initialized: bool,
}
static #base_name_ident: #outer_struct_name = #outer_struct_name {
css: #css,
names: #inner_struct_name {
#(#inner_struct_values)*
},
initialized: false,
};
impl Deref for #outer_struct_name {
type Target = #inner_struct_name;
fn deref(&self) -> &Self::Target {
if !self.initialized {
let init = &self.initialized as *const bool;
let init_mut = init as *mut bool;
unsafe {
*init_mut = true;
}
let window = css_loader::web_sys::window().unwrap();
let document = window.document().unwrap();
let css = document.create_element("style").unwrap();
css.set_text_content(Some(self.css));
document.head().unwrap().append_child(&css).unwrap();
}
&self.names
}
}
}
.into()
}