296 lines
11 KiB
Rust
296 lines
11 KiB
Rust
use std::path::PathBuf;
|
|
|
|
use cfg_if::cfg_if;
|
|
use itertools::Itertools;
|
|
use proc_macro2::{Span, TokenStream};
|
|
use quote::quote;
|
|
use sabry_intrnl::{
|
|
config::SabryConfig,
|
|
scoper::{apply_basic_rusty_member_gen_rules, ArbitraryScope, ScopedSelector},
|
|
};
|
|
use syn::{
|
|
parse::{Parse, ParseStream},
|
|
Ident, Token,
|
|
};
|
|
|
|
use super::{ArbitraryStyleBlock, ArbitraryStyleSyntax};
|
|
|
|
/// Syntax:
|
|
/// `pub? #ident(:$syntax)? { $code } \#?`
|
|
///
|
|
/// `$syntax`: sass/scss
|
|
///
|
|
/// `$code`: intended to be valid based on $syntax
|
|
///
|
|
/// `pub` modifier makes scope public
|
|
///
|
|
/// `const` modifier completely changes the behaviour of styling, and
|
|
/// makes CSS exposed as constant
|
|
///
|
|
/// `#` shebang generates machine-readable and well-parsible but invalid rust code for building purposes
|
|
/// instead of scope module.
|
|
///
|
|
/// The machine-readable output may also be forced by use `machine_readable: true` arg on `parse_macro_syntax` function
|
|
/// without modifying tokenstream
|
|
pub fn styly_macro_impl(input: TokenStream, source_path: Option<PathBuf>) -> TokenStream {
|
|
let config = match SabryConfig::require() {
|
|
Ok(c) => c,
|
|
Err(e) => {
|
|
return syn::Error::new(
|
|
Span::call_site(),
|
|
format!("Could not read sabry configuration required by this macro: {e:?}",),
|
|
)
|
|
.to_compile_error()
|
|
}
|
|
};
|
|
let ms = match parse_macro_syntax(input, source_path) {
|
|
Ok(ms) => ms,
|
|
Err(e) => return e.to_compile_error(),
|
|
};
|
|
|
|
let scope = match ArbitraryScope::from_source(ms.syntax.0, ms.scope.clone(), &ms.code.code) {
|
|
Ok(s) => match s.hashed(&config.hash) {
|
|
Ok(hs) => Ok(hs),
|
|
Err(e) => Err(e),
|
|
},
|
|
Err(e) => Err(e),
|
|
};
|
|
let scope = match scope {
|
|
Ok(s) => s,
|
|
Err(e) => {
|
|
return syn::Error::new(
|
|
Span::call_site(),
|
|
format!("Could not generate scope: {e:?}"),
|
|
)
|
|
.to_compile_error()
|
|
}
|
|
};
|
|
|
|
match ms.generator {
|
|
ScopeGenerator::Module { public, constant } => {
|
|
let scope_hash = scope.hash.as_str();
|
|
let scope_ident = scope.original_scope.name.clone();
|
|
let scope_wrapper_ident =
|
|
syn::parse_str::<Ident>(&scope_ident.to_string().to_uppercase())
|
|
.expect("BUG: We just converted valid Ident to string and uppercased");
|
|
let scope_vis = if public {
|
|
quote! {pub}
|
|
} else {
|
|
quote! {}
|
|
};
|
|
|
|
let nests = scope.original_scope.adapter().nesting_selectors();
|
|
let special_nesting_members = nests
|
|
.iter()
|
|
.filter_map(|ns| {
|
|
ns.suffix.clone().and_then(|s| {
|
|
let interp = match s {
|
|
raffia::ast::InterpolableIdent::SassInterpolated(int) => Some(int),
|
|
_ => None,
|
|
};
|
|
|
|
let interp = interp.and_then(|i| match i.elements.first() {
|
|
Some(raffia::ast::SassInterpolatedIdentElement::Static(i)) => {
|
|
Some(i.raw.to_string())
|
|
}
|
|
_ => None,
|
|
});
|
|
|
|
interp
|
|
})
|
|
})
|
|
.unique()
|
|
.map(|s| {
|
|
let ident = apply_basic_rusty_member_gen_rules(&s);
|
|
let fnident = syn::parse_str::<Ident>(format!("_{ident}").as_str())
|
|
.expect("BUG: invalid ident for nesting selector formed");
|
|
let formatstr = format!("{{c}}{}", &s);
|
|
let doc = format!("Nesting selector. Originated from '&{s}'");
|
|
|
|
quote! {
|
|
#[doc = #doc]
|
|
#[deprecated(
|
|
since = "0.0.6",
|
|
note = "All scoping is done by directly applying hash classes to elements now - this is useless for everything except of classes"
|
|
)]
|
|
pub fn #fnident(c: &str) -> String {format!(#formatstr)}
|
|
}
|
|
});
|
|
|
|
let scope_members = scope
|
|
.hashed_selectors
|
|
.iter()
|
|
// not pretty sure about the best way to do this
|
|
.unique_by(|hs| hs.sel.as_arbitrary().ident.clone())
|
|
.filter_map(|hs| {
|
|
hs.html_ident.clone().map(|html| {
|
|
let name = match hs.sel {
|
|
ScopedSelector::Class(_) => "class",
|
|
ScopedSelector::Id(_) => "id",
|
|
ScopedSelector::Tag(_) => "tagname",
|
|
ScopedSelector::Glob { .. } => "glob",
|
|
};
|
|
|
|
(
|
|
name,
|
|
hs.sel.gen_rusty_ident(),
|
|
html,
|
|
hs.css_ident.clone(),
|
|
)
|
|
})
|
|
})
|
|
.map(|(name, ident, html, css)| {
|
|
let docs = format!("'{ident:?}' {name}. CSS selector '{css}'");
|
|
quote! {
|
|
#[doc = #docs]
|
|
#[deprecated(
|
|
since = "0.0.6",
|
|
note = "All scoping is done by directly applying hash classes to elements now - this is useless for everything except of classes"
|
|
)]
|
|
#[allow(non_upper_case_globals)]
|
|
pub const #ident : &str = #html ;
|
|
}
|
|
});
|
|
|
|
let mod_docs = format!(
|
|
"'{}' style scope. The hash class is `{}`",
|
|
&scope_ident, &scope_wrapper_ident
|
|
);
|
|
let wrp_docs = format!(
|
|
"hash class for '{}' scope. All HTML elements should have this class applied",
|
|
&scope_ident
|
|
);
|
|
|
|
if constant {
|
|
cfg_if! {
|
|
if #[cfg(feature = "const-compile")] {
|
|
use sabry_intrnl::compiler::CompilerAdapter;
|
|
let compiler = CompilerAdapter::new(config.clone());
|
|
let css = match compiler.compile_module(ms.syntax.0, &scope.hashed_code) {
|
|
Ok(c) => c,
|
|
Err(e) => {
|
|
return syn::Error::new(
|
|
ms.code.span,
|
|
format!("Could not compile const inline: {e:?}"),
|
|
)
|
|
.to_compile_error()
|
|
}
|
|
};
|
|
|
|
let const_docs = format!("The compiled CSS style for {} scope", &scope_ident);
|
|
let const_wrapper_ident = syn::parse_str::<Ident>(
|
|
format!("{}_CSS", &scope_ident.to_string().to_uppercase()).as_str(),
|
|
)
|
|
.expect("BUG: We just converted valid Ident to string and uppercased");
|
|
|
|
quote! {
|
|
#[doc = #const_docs]
|
|
#scope_vis const #const_wrapper_ident: &str = #css ;
|
|
#[doc = #wrp_docs]
|
|
#scope_vis const #scope_wrapper_ident : &str = #scope_hash ;
|
|
#[doc = #mod_docs]
|
|
#scope_vis mod #scope_ident {
|
|
#(#scope_members)*
|
|
#(#special_nesting_members)*
|
|
}
|
|
}
|
|
} else {
|
|
return syn::Error::new(
|
|
Span::call_site(),
|
|
"`styly!(const scope...` requires `experimental-const-scoping` feature flag",
|
|
)
|
|
.to_compile_error();
|
|
}
|
|
}
|
|
} else {
|
|
quote! {
|
|
#[doc = #wrp_docs]
|
|
#scope_vis const #scope_wrapper_ident : &str = #scope_hash ;
|
|
#[doc = #mod_docs]
|
|
#scope_vis mod #scope_ident {
|
|
#(#scope_members)*
|
|
#(#special_nesting_members)*
|
|
}
|
|
}
|
|
}
|
|
}
|
|
ScopeGenerator::MachineReadable => {
|
|
quote! {}
|
|
}
|
|
}
|
|
}
|
|
|
|
// reusable function, that does not return tokenstream for machine-processing at build time
|
|
pub fn parse_macro_syntax(
|
|
input: TokenStream,
|
|
source_path: Option<PathBuf>,
|
|
) -> Result<MacroSyntax, syn::Error> {
|
|
syn::parse::Parser::parse2(
|
|
|input: ParseStream<'_>| MacroSyntax::parse_syn(input, source_path),
|
|
input,
|
|
)
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum ScopeGenerator {
|
|
Module { public: bool, constant: bool },
|
|
MachineReadable,
|
|
}
|
|
|
|
impl Parse for ScopeGenerator {
|
|
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
|
let public = input.parse::<Token![pub]>().is_ok();
|
|
let constant = input.parse::<Token![const]>().is_ok();
|
|
|
|
// would be nice to implement compile-time CSS compilation to const
|
|
// with `const` modifier, so I'll leave it this way for now
|
|
let this = Self::Module { public, constant };
|
|
|
|
Ok(this)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct MacroSyntax {
|
|
/// The way macro should be expanded
|
|
pub generator: ScopeGenerator,
|
|
/// Scope identifier
|
|
pub scope: Ident,
|
|
/// Used stytax
|
|
pub syntax: ArbitraryStyleSyntax,
|
|
/// Style code, either from rust or read from file
|
|
pub code: ArbitraryStyleBlock,
|
|
}
|
|
|
|
impl MacroSyntax {
|
|
pub fn parse_syn(
|
|
input: syn::parse::ParseStream,
|
|
source_path: Option<PathBuf>,
|
|
) -> syn::Result<Self> {
|
|
let mut generator = input.parse::<ScopeGenerator>()?;
|
|
let scope = input.parse::<Ident>()?;
|
|
let syntax = input.parse::<ArbitraryStyleSyntax>()?;
|
|
let code = if source_path.is_some() {
|
|
ArbitraryStyleBlock::parse_syn(input, source_path)
|
|
} else {
|
|
input.parse::<ArbitraryStyleBlock>()
|
|
}?;
|
|
|
|
if input.parse::<Token![#]>().is_ok() {
|
|
generator = ScopeGenerator::MachineReadable
|
|
}
|
|
|
|
Ok(Self {
|
|
generator,
|
|
scope,
|
|
syntax,
|
|
code,
|
|
})
|
|
}
|
|
}
|
|
|
|
impl Parse for MacroSyntax {
|
|
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
|
Self::parse_syn(input, None)
|
|
}
|
|
}
|