turf_macros 0.10.1

Macros used by turf.
Documentation
//! You're probably looking for `turf` instead.

use convert_case::{Case, Casing};
use std::{collections::HashMap, path::PathBuf};
use turf_internals::{CompiledStyleSheet, StyleSheetKind};

use proc_macro::TokenStream;
use quote::quote;

#[proc_macro]
pub fn style_sheet(input: TokenStream) -> TokenStream {
    let input = input.to_string();
    let sanitized_path = PathBuf::from(input.trim_matches('"'));

    let ProcessedStyleSheet {
        untracked_load_paths,
        css,
        class_names,
    } = match handle_style_sheet(StyleSheetKind::File(sanitized_path)) {
        Ok(result) => result,
        Err(e) => {
            return match e {
                Error::Turf(e) => to_compile_error(e),
                Error::LoadPathTracking(e) => to_compile_error(e),
            }
        }
    };

    let mut out = quote! {
        pub static STYLE_SHEET: &'static str = #css;
    };
    out.extend(create_classes_structure(class_names));
    out.extend(create_include_bytes(untracked_load_paths));

    out.into()
}

#[proc_macro]
pub fn style_sheet_values(input: TokenStream) -> TokenStream {
    let input = input.to_string();
    let sanitized_path = PathBuf::from(input.trim_matches('"'));

    let ProcessedStyleSheet {
        untracked_load_paths,
        css,
        class_names,
    } = match handle_style_sheet(StyleSheetKind::File(sanitized_path)) {
        Ok(result) => result,
        Err(e) => {
            return match e {
                Error::Turf(e) => to_compile_error(e),
                Error::LoadPathTracking(e) => to_compile_error(e),
            }
        }
    };

    let includes = create_include_bytes(untracked_load_paths);
    let inlines = create_inline_classes_instance(class_names);
    let out = quote! {{
        pub static STYLE_SHEET: &'static str = #css;
        #includes
        #inlines
    }};

    out.into()
}

#[proc_macro]
pub fn inline_style_sheet(input: TokenStream) -> TokenStream {
    let input = input.to_string();

    let ProcessedStyleSheet {
        untracked_load_paths,
        css,
        class_names,
    } = match handle_style_sheet(StyleSheetKind::Inline(input)) {
        Ok(result) => result,
        Err(e) => {
            return match e {
                Error::Turf(e) => to_compile_error(e),
                Error::LoadPathTracking(e) => to_compile_error(e),
            }
        }
    };

    let mut out = quote! {
        pub static STYLE_SHEET: &'static str = #css;
    };
    out.extend(create_classes_structure(class_names));
    out.extend(create_include_bytes(untracked_load_paths));

    out.into()
}

#[proc_macro]
pub fn inline_style_sheet_values(input: TokenStream) -> TokenStream {
    let input = input.to_string();

    let ProcessedStyleSheet {
        untracked_load_paths,
        css,
        class_names,
    } = match handle_style_sheet(StyleSheetKind::Inline(input)) {
        Ok(result) => result,
        Err(e) => {
            return match e {
                Error::Turf(e) => to_compile_error(e),
                Error::LoadPathTracking(e) => to_compile_error(e),
            }
        }
    };

    let includes = create_include_bytes(untracked_load_paths);
    let inlines = create_inline_classes_instance(class_names);
    let out = quote! {{
        pub static STYLE_SHEET: &'static str = #css;
        #includes
        #inlines
    }};

    out.into()
}

fn to_compile_error<E>(e: E) -> TokenStream
where
    E: std::error::Error,
{
    let mut message = format!("Error: {}", e);
    let mut curr_err = e.source();

    if curr_err.is_some() {
        message.push_str("\nCaused by:");
    }

    while let Some(current_error) = curr_err {
        message.push_str(&format!("\n    {}", current_error));
        curr_err = current_error.source();
    }

    quote! {
        compile_error!(#message);
    }
    .into()
}

fn create_classes_structure(classes: HashMap<String, String>) -> proc_macro2::TokenStream {
    let original_class_names: Vec<proc_macro2::Ident> = classes
        .keys()
        .map(|class| class.to_case(Case::ScreamingSnake))
        .map(|class| quote::format_ident!("{}", class.as_str().to_uppercase()))
        .collect();

    let randomized_class_names: Vec<&String> = classes.values().collect();

    let doc = original_class_names
        .iter()
        .zip(randomized_class_names.iter())
        .fold(String::new(), |mut doc, (variable, class_name)| {
            doc.push_str(&format!("{} = \"{}\"\n", variable, class_name));
            doc
        });

    quote::quote! {
        #[doc=#doc]
        pub struct ClassName;
        impl ClassName {
            #(pub const #original_class_names: &'static str = #randomized_class_names;)*
        }
    }
}

fn create_inline_classes_instance(classes: HashMap<String, String>) -> proc_macro2::TokenStream {
    let original_class_names: Vec<proc_macro2::Ident> = classes
        .keys()
        .map(|class| class.to_case(Case::Snake))
        .map(|class| quote::format_ident!("{}", class.as_str()))
        .collect();

    let randomized_class_names: Vec<&String> = classes.values().collect();

    let doc = original_class_names
        .iter()
        .zip(randomized_class_names.iter())
        .fold(String::new(), |mut doc, (variable, class_name)| {
            doc.push_str(&format!("{} = \"{}\"\n", variable, class_name));
            doc
        });

    quote::quote! {
        #[doc=#doc]
        pub struct ClassNames {
            #(pub #original_class_names: &'static str,)*
        }
        impl ClassNames {
            pub fn new() -> Self {
                Self {
                    #(#original_class_names: #randomized_class_names,)*
                }
            }
        }

        (STYLE_SHEET, ClassNames::new())
    }
}

fn create_include_bytes(untracked_load_paths: Vec<PathBuf>) -> proc_macro2::TokenStream {
    let untracked_load_path_values: Vec<String> = untracked_load_paths
        .into_iter()
        .map(|item| format!("{}", item.as_path().display()))
        .collect();

    quote::quote! {
        #(const _: &[u8] = include_bytes!(#untracked_load_path_values);)*
    }
}

enum Error {
    Turf(turf_internals::Error),
    LoadPathTracking(turf_internals::LoadPathTrackingError),
}

struct ProcessedStyleSheet {
    untracked_load_paths: Vec<PathBuf>,
    css: String,
    class_names: HashMap<String, String>,
}

fn handle_style_sheet(style_sheet: StyleSheetKind) -> Result<ProcessedStyleSheet, Error> {
    let CompiledStyleSheet {
        css,
        class_names,
        original_style_sheet,
    } = turf_internals::style_sheet(style_sheet).map_err(Error::Turf)?;

    let untracked_load_paths = {
        let mut values =
            turf_internals::get_untracked_load_paths().map_err(Error::LoadPathTracking)?;

        if let StyleSheetKind::File(current_file_path) = original_style_sheet {
            values.push(current_file_path);
        }

        values
    };

    Ok(ProcessedStyleSheet {
        untracked_load_paths,
        css,
        class_names,
    })
}

#[cfg(test)]
mod tests {
    use std::collections::HashMap;

    use super::create_classes_structure;

    #[test]
    fn test() {
        let mut class_names = HashMap::new();
        class_names.insert(String::from("test-class"), String::from("abc-123"));

        let out = create_classes_structure(class_names);

        assert_eq!(
            out.to_string(),
            quote::quote! {
                #[doc="TEST_CLASS = \"abc-123\"\n"]
                pub struct ClassName;
                impl ClassName {
                    pub const TEST_CLASS: &'static str = "abc-123";
                }
            }
            .to_string()
        )
    }
}