use regex::Regex;
use std::any;
use std::cell::RefCell;
use std::collections::hash_map::DefaultHasher;
use std::collections::HashSet;
use std::hash::Hasher;
use wasm_bindgen::{prelude::*, JsCast};
thread_local! {
static STYLED: RefCell<HashSet<u64>> = RefCell::new(HashSet::new());
static SHEET: RefCell<Option<web_sys::CssStyleSheet>> = RefCell::new(None);
static CLASS_SELECTER: Regex = Regex::new(r"\.([a-zA-Z][a-zA-Z0-9\-_]*)").unwrap();
}
fn hash_of_type<C>() -> u64 {
let mut hasher = DefaultHasher::new();
hasher.write(any::type_name::<C>().as_bytes());
hasher.finish()
}
fn styled_class_prefix<C>() -> String {
let mut hasher = DefaultHasher::new();
hasher.write(any::type_name::<C>().as_bytes());
format!("{:X}", hasher.finish())
}
fn styled_class<C>(class_name: &str) -> String {
format!("_{}__{}", styled_class_prefix::<C>(), class_name)
}
pub trait Styled: Sized {
fn style() -> Style;
fn styled<T>(node: T) -> T {
STYLED.with(|styled| {
let component_id = hash_of_type::<Self>();
if styled.borrow().get(&component_id).is_none() {
let style = Self::style();
style.write::<Self>();
styled.borrow_mut().insert(component_id);
}
});
node
}
fn class(class_name: &str) -> String {
styled_class::<Self>(class_name)
}
}
#[derive(Clone)]
pub struct Style {
style: Vec<(String, Vec<(String, String)>)>,
media: Vec<(String, Self)>,
}
impl Style {
pub fn new() -> Self {
Self {
style: vec![],
media: vec![],
}
}
pub fn add(
&mut self,
selector: impl Into<String>,
property: impl Into<String>,
value: impl Into<String>,
) {
let selector = selector.into();
let property = property.into();
let value = value.into();
if let Some(class_idx) = self.style.iter().position(|s| s.0 == selector) {
if let Some(property_idx) = self.style[class_idx].1.iter().position(|p| p.0 == property)
{
self.style[class_idx].1[property_idx].1 = value;
} else {
self.style[class_idx].1.push((property, value));
}
} else {
self.style.push((selector, vec![(property, value)]));
}
}
pub fn add_media(&mut self, query: impl Into<String>, style: Style) {
let query = query.into();
self.media.push((query, style));
}
pub fn append(&mut self, other: &Self) {
for (selector, definition_block) in &other.style {
for (property, value) in definition_block {
self.add(selector, property, value);
}
}
for (query, style) in &other.media {
self.add_media(query, style.clone());
}
}
fn rules<C>(&self) -> Vec<String> {
let mut res = vec![];
for (selector, definition_block) in &self.style {
let mut rule = String::new();
let selector = CLASS_SELECTER.with(|class_selecter| {
class_selecter.replace_all(
selector,
format!("._{}__$1", styled_class_prefix::<C>()).as_str(),
)
});
rule += &selector;
rule += "{";
for (property, value) in definition_block {
rule += format!("{}:{};", property, value).as_str();
}
rule += "}";
res.push(rule);
}
for (query, media_style) in &self.media {
let mut rule = String::from("@media ");
rule += query;
rule += "{";
for child_rule in &media_style.rules::<C>() {
rule += child_rule;
}
rule += "}";
res.push(rule);
}
res
}
fn write<C>(&self) {
Self::add_style_element();
for rule in &self.rules::<C>() {
SHEET.with(|sheet| {
if let Some(sheet) = sheet.borrow().as_ref() {
if let Err(err) = sheet
.insert_rule_with_index(rule.as_str(), sheet.css_rules().unwrap().length())
{
web_sys::console::log_1(&JsValue::from(err));
}
}
});
}
}
fn add_style_element() {
SHEET.with(|sheet| {
if sheet.borrow().is_none() {
let style_element = web_sys::window()
.unwrap()
.document()
.unwrap()
.create_element("style")
.unwrap()
.dyn_into::<web_sys::HtmlStyleElement>()
.unwrap();
let head = web_sys::window()
.unwrap()
.document()
.unwrap()
.get_elements_by_tag_name("head")
.item(0)
.unwrap();
let _ = head.append_child(&style_element);
*sheet.borrow_mut() = Some(
style_element
.sheet()
.unwrap()
.dyn_into::<web_sys::CssStyleSheet>()
.unwrap(),
);
}
});
}
}
macro_rules! return_if {
($x:ident = $y:expr; $z: expr) => {{
let $x = $y;
if $z {
return $x;
}
}};
}
impl std::fmt::Debug for Style {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for (selector, definition_block) in &self.style {
return_if!(x = write!(f, "{} {}\n", selector, "{"); x.is_err());
for (property, value) in definition_block {
return_if!(x = write!(f, " {}: {};\n", property, value); x.is_err());
}
return_if!(x = write!(f, "{}\n", "}"); x.is_err());
}
for (query, style) in &self.media {
return_if!(x = write!(f, "@media {} {}\n", query, "{"); x.is_err());
for a_line in format!("{:?}", style).split("\n") {
if a_line != "" {
return_if!(x = write!(f, " {}\n", a_line); x.is_err());
}
}
return_if!(x = write!(f, "{}\n", "}"); x.is_err());
}
write!(f, "")
}
}
impl PartialEq for Style {
fn eq(&self, other: &Self) -> bool {
self.style.eq(&other.style) && self.media.eq(&other.media)
}
}
#[macro_export]
macro_rules! style {
{
$(
@import $import:expr;
)*
$(
$selector:literal {$(
$property:tt : $value:expr
);*;}
)*
$(
@media $query:tt {$(
$media_style:tt
)*}
)*
} => {{
#[allow(unused_mut)]
let mut style = Style::new();
$(
style.append(&($import));
)*
$(
$(
style.add(format!("{}", $selector), format!("{}", $property), format!("{}", $value));
)*
)*
$(
style.add_media($query, style!{$($media_style)*});
)*
style
}};
}
#[cfg(test)]
mod tests {
use super::Style;
#[test]
fn it_works() {
assert!(true);
}
#[test]
fn debug_style() {
let style = Style {
style: vec![
(
String::from("foo"),
vec![
(String::from("width"), String::from("100px")),
(String::from("height"), String::from("100px")),
],
),
(
String::from("bar"),
vec![
(String::from("width"), String::from("100px")),
(String::from("height"), String::from("100px")),
],
),
],
media: vec![],
};
let style_str = concat!(
"foo {\n",
" width: 100px;\n",
" height: 100px;\n",
"}\n",
"bar {\n",
" width: 100px;\n",
" height: 100px;\n",
"}\n",
);
assert_eq!(format!("{:?}", style), style_str);
}
#[test]
fn debug_style_with_media() {
let media_style = Style {
style: vec![
(
String::from("foo"),
vec![
(String::from("width"), String::from("100px")),
(String::from("height"), String::from("100px")),
],
),
(
String::from("bar"),
vec![
(String::from("width"), String::from("100px")),
(String::from("height"), String::from("100px")),
],
),
],
media: vec![],
};
let style = Style {
style: vec![
(
String::from("foo"),
vec![
(String::from("width"), String::from("100px")),
(String::from("height"), String::from("100px")),
],
),
(
String::from("bar"),
vec![
(String::from("width"), String::from("100px")),
(String::from("height"), String::from("100px")),
],
),
],
media: vec![(String::from("query"), media_style)],
};
let style_str = concat!(
"foo {\n",
" width: 100px;\n",
" height: 100px;\n",
"}\n",
"bar {\n",
" width: 100px;\n",
" height: 100px;\n",
"}\n",
"@media query {\n",
" foo {\n",
" width: 100px;\n",
" height: 100px;\n",
" }\n",
" bar {\n",
" width: 100px;\n",
" height: 100px;\n",
" }\n",
"}\n",
);
assert_eq!(format!("{:?}", style), style_str);
}
#[test]
fn gen_style_by_manual() {
let style_a = Style {
style: vec![
(
String::from("foo"),
vec![
(String::from("width"), String::from("100px")),
(String::from("height"), String::from("100px")),
],
),
(
String::from("bar"),
vec![
(String::from("width"), String::from("100px")),
(String::from("height"), String::from("100px")),
],
),
],
media: vec![],
};
let mut style_b = Style::new();
style_b.add("foo", "width", "100px");
style_b.add("foo", "height", "100px");
style_b.add("bar", "width", "100px");
style_b.add("bar", "height", "100px");
assert_eq!(style_a, style_b);
}
#[test]
fn gen_style_with_media_by_manual() {
let media_style_a = Style {
style: vec![
(
String::from("foo"),
vec![
(String::from("width"), String::from("100px")),
(String::from("height"), String::from("100px")),
],
),
(
String::from("bar"),
vec![
(String::from("width"), String::from("100px")),
(String::from("height"), String::from("100px")),
],
),
],
media: vec![],
};
let style_a = Style {
style: vec![
(
String::from("foo"),
vec![
(String::from("width"), String::from("100px")),
(String::from("height"), String::from("100px")),
],
),
(
String::from("bar"),
vec![
(String::from("width"), String::from("100px")),
(String::from("height"), String::from("100px")),
],
),
],
media: vec![(String::from("query"), media_style_a)],
};
let mut media_style_b = Style::new();
media_style_b.add("foo", "width", "100px");
media_style_b.add("foo", "height", "100px");
media_style_b.add("bar", "width", "100px");
media_style_b.add("bar", "height", "100px");
let mut style_b = Style::new();
style_b.add("foo", "width", "100px");
style_b.add("foo", "height", "100px");
style_b.add("bar", "width", "100px");
style_b.add("bar", "height", "100px");
style_b.add_media("query", media_style_b);
assert_eq!(style_a, style_b);
}
#[test]
fn gen_style_by_macro() {
let mut style_a = Style::new();
style_a.add("foo", "width", "100px");
style_a.add("foo", "height", "100px");
style_a.add("bar", "width", "100px");
style_a.add("bar", "height", "100px");
let style_b = style! {
"foo" {
"width": "100px";
"height": "100px";
}
"bar" {
"width": "100px";
"height": "100px";
}
};
assert_eq!(style_a, style_b);
}
#[test]
fn gen_style_with_media_by_macro() {
let mut media_style_a = Style::new();
media_style_a.add("foo", "width", "100px");
media_style_a.add("foo", "height", "100px");
media_style_a.add("bar", "width", "100px");
media_style_a.add("bar", "height", "100px");
let mut style_a = Style::new();
style_a.add("foo", "width", "100px");
style_a.add("foo", "height", "100px");
style_a.add("bar", "width", "100px");
style_a.add("bar", "height", "100px");
style_a.add_media("query", media_style_a);
let style_b = style! {
"foo" {
"width": "100px";
"height": "100px";
}
"bar" {
"width": "100px";
"height": "100px";
}
@media "query" {
"foo" {
"width": "100px";
"height": "100px";
}
"bar" {
"width": "100px";
"height": "100px";
}
}
};
assert_eq!(style_a, style_b);
}
}