use crate::*;
pub(crate) fn is_double_colon(content: ParseStream) -> bool {
let forked: ParseBuffer<'_> = content.fork();
let _: Ident = match forked.parse() {
Ok(ident) => ident,
Err(_) => return false,
};
forked.peek(Token![::])
}
pub(crate) fn set_user_fn_names(names: HashMap<String, ComponentInfo>) {
unsafe {
let pointer: *mut MaybeUninit<HashMap<String, ComponentInfo>> = &raw mut USER_FN_NAMES;
(*pointer).write(names);
}
}
pub(crate) fn get_loaded_component_registry() -> HashMap<String, ComponentInfo> {
unsafe {
let pointer: *const MaybeUninit<HashMap<String, ComponentInfo>> = &raw const USER_FN_NAMES;
(*pointer).assume_init_ref().clone()
}
}
pub(crate) fn is_user_fn(name: &str) -> bool {
unsafe {
let pointer: *const MaybeUninit<HashMap<String, ComponentInfo>> = &raw const USER_FN_NAMES;
(*pointer).assume_init_ref().contains_key(name)
}
}
pub(crate) fn get_user_fn_props_type(name: &str) -> Option<&'static str> {
unsafe {
let pointer: *const MaybeUninit<HashMap<String, ComponentInfo>> = &raw const USER_FN_NAMES;
(*pointer)
.assume_init_ref()
.get(name)
.map(|info: &ComponentInfo| info.get_props_type().as_str())
}
}
pub(crate) fn get_user_fn_props_fields(name: &str) -> Option<&'static Vec<String>> {
unsafe {
let pointer: *const MaybeUninit<HashMap<String, ComponentInfo>> = &raw const USER_FN_NAMES;
(*pointer)
.assume_init_ref()
.get(name)
.map(|info: &ComponentInfo| info.get_props_fields())
}
}
pub(crate) fn get_user_fn_props_field_types(
name: &str,
) -> Option<&'static HashMap<String, String>> {
unsafe {
let pointer: *const MaybeUninit<HashMap<String, ComponentInfo>> = &raw const USER_FN_NAMES;
(*pointer)
.assume_init_ref()
.get(name)
.map(|info: &ComponentInfo| info.get_props_field_types())
}
}
pub fn parse_html(input: TokenStream) -> TokenStream {
let fn_names: HashMap<String, ComponentInfo> = load_component_registry();
set_user_fn_names(fn_names);
let tokens: proc_macro2::TokenStream = match parse::<HtmlRoot>(input) {
Ok(nodes) => nodes.into_token_stream(),
Err(error) => return error.to_compile_error().into(),
};
TokenStream::from(tokens)
}
pub(crate) fn load_component_registry() -> HashMap<String, ComponentInfo> {
let Ok(manifest_dir) = env::var(CARGO_MANIFEST_DIR) else {
return HashMap::new();
};
let src_dir: PathBuf = PathBuf::from(&manifest_dir).join(SRC_DIR);
let mut rust_source_files: Vec<PathBuf> = Vec::new();
collect_rs_files(&src_dir, &mut rust_source_files);
let fingerprint: String = compute_fingerprint(&rust_source_files);
if let Ok(out_dir) = env::var(ENV_OUT_DIR) {
let cache_path: PathBuf = PathBuf::from(out_dir).join(REGISTRY_CACHE_FILE_NAME);
if let Some(cached) = try_load_cache(&cache_path, &fingerprint) {
return cached;
}
let registry: HashMap<String, ComponentInfo> =
build_registry_from_files(&rust_source_files);
try_save_cache(&cache_path, &fingerprint, ®istry);
registry
} else {
build_registry_from_files(&rust_source_files)
}
}
fn compute_fingerprint(files: &[PathBuf]) -> String {
let mut fingerprint: String = String::new();
let mut sorted_files: Vec<&PathBuf> = files.iter().collect();
sorted_files.sort();
for path in sorted_files {
fingerprint.push_str(&path.to_string_lossy());
fingerprint.push(CHAR_SEMICOLON);
if let Ok(metadata) = std::fs::metadata(path)
&& let Ok(modified) = metadata.modified()
&& let Ok(duration) = modified.duration_since(std::time::UNIX_EPOCH)
{
fingerprint.push_str(&duration.as_millis().to_string());
}
fingerprint.push(CHAR_SEMICOLON);
}
fingerprint
}
fn try_load_cache(
cache_path: &PathBuf,
current_fingerprint: &str,
) -> Option<HashMap<String, ComponentInfo>> {
let content: String = read_to_string(cache_path).ok()?;
let (stored_fingerprint, data) = content.split_once(CHAR_NEWLINE)?;
if stored_fingerprint != current_fingerprint {
return None;
}
serde_json::from_str(data).ok()
}
fn try_save_cache(
cache_path: &PathBuf,
fingerprint: &str,
registry: &HashMap<String, ComponentInfo>,
) {
if let Ok(data) = serde_json::to_string(registry) {
let content: String = format!("{fingerprint}{CHAR_NEWLINE}{data}");
let _ = std::fs::write(cache_path, content);
}
}
fn build_registry_from_files(files: &[PathBuf]) -> HashMap<String, ComponentInfo> {
let mut global_props_fields_map: HashMap<String, Vec<String>> = HashMap::new();
let mut global_props_field_types_map: HashMap<String, HashMap<String, String>> = HashMap::new();
let mut component_entries: Vec<(String, String)> = Vec::new();
for path in files {
let Ok(content) = read_to_string(path) else {
continue;
};
let Ok(file) = parse_file(&content) else {
continue;
};
global_props_fields_map.extend(extract_props_structs(&file));
global_props_field_types_map.extend(extract_props_struct_types(&file));
extract_component_entries(&file, &mut component_entries);
}
component_entries
.into_iter()
.map(|(fn_name, props_type): (String, String)| {
let props_fields: Vec<String> = global_props_fields_map
.get(&props_type)
.cloned()
.unwrap_or_default();
let props_field_types: HashMap<String, String> = global_props_field_types_map
.get(&props_type)
.cloned()
.unwrap_or_default();
(
fn_name,
ComponentInfo {
props_type,
props_fields,
props_field_types,
},
)
})
.collect()
}
fn extract_component_entries(file: &File, entries: &mut Vec<(String, String)>) {
file.items
.iter()
.filter_map(|item: &Item| {
let Item::Fn(item_fn) = item else {
return None;
};
item_fn
.attrs
.iter()
.any(|attr: &Attribute| attr.path().is_ident(COMPONENT_ATTR))
.then(|| {
let fn_name: String = item_fn.sig.ident.to_string();
let props_type: String = extract_props_type_from_fn(item_fn);
(fn_name, props_type)
})
})
.for_each(|entry: (String, String)| {
entries.push(entry);
});
}
fn collect_rs_files(dir: &PathBuf, files: &mut Vec<PathBuf>) {
let Ok(entries) = read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path: PathBuf = entry.path();
if path.is_dir() {
collect_rs_files(&path, files);
} else if path
.extension()
.is_some_and(|ext: &OsStr| ext == OsStr::new(RUST_FILE_EXTENSION))
{
files.push(path);
}
}
}
fn extract_props_structs(file: &File) -> HashMap<String, Vec<String>> {
file.items
.iter()
.filter_map(|item: &Item| {
let Item::Struct(item_struct) = item else {
return None;
};
Some((
item_struct.ident.to_string(),
item_struct
.fields
.iter()
.filter_map(|field: &Field| {
field.ident.as_ref().map(|ident: &Ident| ident.to_string())
})
.collect(),
))
})
.collect()
}
fn extract_props_struct_types(file: &File) -> HashMap<String, HashMap<String, String>> {
file.items
.iter()
.filter_map(|item: &Item| {
let Item::Struct(item_struct) = item else {
return None;
};
Some((
item_struct.ident.to_string(),
item_struct
.fields
.iter()
.filter_map(|field: &Field| {
field.ident.as_ref().map(|ident: &Ident| {
(ident.to_string(), extract_type_last_segment(&field.ty))
})
})
.collect(),
))
})
.collect()
}
fn extract_type_last_segment(param_type: &Type) -> String {
if let Type::Path(type_path) = param_type
&& let Some(segment) = type_path.path.segments.last()
{
return segment.ident.to_string();
}
param_type
.to_token_stream()
.to_string()
.replace(CHAR_SPACE, STR_EMPTY)
}
fn extract_props_type_from_fn(item_fn: &syn::ItemFn) -> String {
let inputs: &syn::punctuated::Punctuated<syn::FnArg, Token![,]> = &item_fn.sig.inputs;
for input in inputs {
if let syn::FnArg::Typed(pat_type) = input {
let param_type: &Type = &pat_type.ty;
if let Type::Path(type_path) = param_type
&& let Some(segment) = type_path.path.segments.last()
&& segment.ident == VIRTUAL_NODE_TYPE
{
if let syn::PathArguments::AngleBracketed(args) = &segment.arguments
&& let Some(syn::GenericArgument::Type(inner_param_type)) = args.args.first()
&& let Type::Path(inner_path) = inner_param_type
&& let Some(inner_segment) = inner_path.path.segments.last()
{
return inner_segment.ident.to_string();
}
} else if let Type::Path(type_path) = param_type
&& let Some(segment) = type_path.path.segments.last()
{
return segment.ident.to_string();
}
}
}
String::new()
}
pub(crate) fn is_dynamic_tag_pattern(second_brace: ParseStream, outer: ParseStream) -> bool {
second_brace.is_empty()
|| (second_brace.peek(Ident)
&& (second_brace.peek2(Colon) || second_brace.peek2(Token![-])))
|| second_brace.peek(Token![if])
|| second_brace.peek(Token![match])
|| second_brace.peek(Token![for])
|| second_brace.peek(LitStr)
|| (second_brace.peek(Brace) && outer.peek2(Colon))
|| (second_brace.peek(Brace) && second_brace.peek2(Brace))
}
pub(crate) fn parse_html_children(content: ParseStream) -> syn::Result<Vec<HtmlNode>> {
let mut children: Vec<HtmlNode> = Vec::new();
while !content.is_empty() {
if content.peek(Brace) && content.peek2(Brace) {
let forked: ParseBuffer<'_> = content.fork();
let _first_brace: ParseBuffer<'_>;
braced!(_first_brace in forked);
let second_brace: ParseBuffer<'_>;
braced!(second_brace in forked);
if is_dynamic_tag_pattern(&second_brace, content) {
let tag_content: ParseBuffer<'_>;
braced!(tag_content in content);
let tag_expr: Expr = tag_content.parse()?;
let body_content: ParseBuffer<'_>;
braced!(body_content in content);
let (dynamic_attrs, dynamic_children): (HtmlAttrs, Vec<HtmlNode>) =
parse_dynamic_component_children(&body_content)?;
children.push(HtmlNode::DynamicTag(HtmlDynamicTag::new(
tag_expr,
dynamic_attrs,
dynamic_children,
)));
} else {
let child_content: ParseBuffer<'_>;
braced!(child_content in content);
let expr: Expr = child_content.parse()?;
children.push(HtmlNode::Dynamic(expr));
}
} else if content.peek(LitStr) && content.peek2(Brace) {
let element: HtmlElement = content.parse()?;
children.push(HtmlNode::Element(element));
} else if content.peek(LitStr) {
let literal_string: LitStr = content.parse()?;
children.push(HtmlNode::Text(literal_string.value()));
} else if content.peek(Token![if]) {
let html_if: HtmlIf = content.parse()?;
children.push(HtmlNode::If(html_if));
} else if content.peek(Token![match]) {
let html_match: HtmlMatch = content.parse()?;
children.push(HtmlNode::Match(html_match));
} else if content.peek(Token![for]) {
let html_for: HtmlFor = content.parse()?;
children.push(HtmlNode::For(html_for));
} else if content.peek(Brace) {
let child_content: ParseBuffer<'_>;
braced!(child_content in content);
let expr: Expr = child_content.parse()?;
children.push(HtmlNode::Dynamic(expr));
} else if (content.peek(Ident) || content.peek(LitStr))
&& content.peek2(Colon)
&& !is_double_colon(content)
{
break;
} else if content.peek(Ident) {
if content.peek2(Brace) {
let element: HtmlElement = content.parse()?;
children.push(HtmlNode::Element(element));
} else {
let expr: Expr = content.parse()?;
children.push(HtmlNode::Expr(expr));
}
} else {
return Err(content.error(ERR_UNEXPECTED_TOKEN_IN_HTML));
}
}
Ok(children)
}
pub(crate) fn parse_match_arm_body(content: ParseStream) -> syn::Result<Vec<HtmlNode>> {
if content.peek(Brace) {
let child_content: ParseBuffer<'_>;
braced!(child_content in content);
parse_html_children(&child_content)
} else {
let node: HtmlNode = content.parse()?;
Ok(vec![node])
}
}
pub(crate) fn parse_dynamic_component_children(
content: ParseStream,
) -> syn::Result<(HtmlAttrs, Vec<HtmlNode>)> {
let mut attributes: HtmlAttrs = Vec::new();
let mut children: Vec<HtmlNode> = Vec::new();
while !content.is_empty() {
if content.peek(Brace) && content.peek2(Brace) {
let forked: ParseBuffer<'_> = content.fork();
let _first_brace: ParseBuffer<'_>;
braced!(_first_brace in forked);
let second_brace: ParseBuffer<'_>;
braced!(second_brace in forked);
if is_dynamic_tag_pattern(&second_brace, content) {
let tag_content: ParseBuffer<'_>;
braced!(tag_content in content);
let tag_expr: Expr = tag_content.parse()?;
let body_content: ParseBuffer<'_>;
braced!(body_content in content);
let (dynamic_attrs, dynamic_children): (HtmlAttrs, Vec<HtmlNode>) =
parse_dynamic_component_children(&body_content)?;
children.push(HtmlNode::DynamicTag(HtmlDynamicTag::new(
tag_expr,
dynamic_attrs,
dynamic_children,
)));
} else {
let child_content: ParseBuffer<'_>;
braced!(child_content in content);
let expr: Expr = child_content.parse()?;
children.push(HtmlNode::Dynamic(expr));
}
} else if content.peek(Token![if]) {
let html_if: HtmlIf = content.parse()?;
children.push(HtmlNode::If(html_if));
} else if content.peek(Token![match]) {
let html_match: HtmlMatch = content.parse()?;
children.push(HtmlNode::Match(html_match));
} else if content.peek(Token![for]) {
let html_for: HtmlFor = content.parse()?;
children.push(HtmlNode::For(html_for));
} else if content.peek(Brace) && content.peek2(Colon) {
let key_content: ParseBuffer<'_>;
braced!(key_content in content);
let key_expr: Expr = key_content.parse()?;
content.parse::<Colon>()?;
let value: HtmlAttrValue = parse_attr_value(content, STR_EMPTY)?;
attributes.push((key_expr.to_token_stream(), value));
} else if content.peek(Brace) {
let child_content: ParseBuffer<'_>;
braced!(child_content in content);
let expr: Expr = child_content.parse()?;
children.push(HtmlNode::Dynamic(expr));
} else if content.peek(LitStr) && content.peek2(Brace) {
let element: HtmlElement = content.parse()?;
children.push(HtmlNode::Element(element));
} else if content.peek(LitStr) && content.peek2(Colon) {
let key_literal: LitStr = content.parse()?;
let key_str: String = key_literal.value();
content.parse::<Colon>()?;
let value: HtmlAttrValue = parse_attr_value(content, &key_str)?;
attributes.push((key_literal.to_token_stream(), value));
} else if content.peek(Ident)
&& (content.peek2(Colon) || content.peek2(Token![-]))
&& !is_double_colon(content)
{
let key_string: String = parse_kebab_name(content)?;
let key_literal: LitStr = LitStr::new(&key_string, content.span());
content.parse::<Colon>()?;
let key_str: String = key_string
.strip_prefix(RAW_IDENT_PREFIX)
.unwrap_or(&key_string)
.to_string();
let value: HtmlAttrValue = parse_attr_value(content, &key_str)?;
attributes.push((key_literal.to_token_stream(), value));
} else if content.peek(LitStr) {
let literal_string: LitStr = content.parse()?;
children.push(HtmlNode::Text(literal_string.value()));
} else if content.peek(Ident) {
if content.peek2(Brace) {
let element: HtmlElement = content.parse()?;
children.push(HtmlNode::Element(element));
} else {
let expr: Expr = content.parse()?;
children.push(HtmlNode::Expr(expr));
}
} else {
return Err(content.error(ERR_UNEXPECTED_TOKEN_IN_DYNAMIC_COMPONENT));
}
}
let merged_attributes: HtmlAttrs = merge_same_key_attributes(attributes);
Ok((merged_attributes, children))
}
pub(crate) fn nodes_to_token_vec(children: &[HtmlNode]) -> Vec<proc_macro2::TokenStream> {
children
.iter()
.map(|child: &HtmlNode| {
let mut token_stream: proc_macro2::TokenStream = proc_macro2::TokenStream::new();
child.to_tokens(&mut token_stream);
token_stream
})
.collect()
}
pub(crate) fn children_to_node_tokens(children: &[HtmlNode]) -> proc_macro2::TokenStream {
match children.len() {
0 => quote! { ::euv::VirtualNode::Empty },
1 => {
let mut token_stream: proc_macro2::TokenStream = proc_macro2::TokenStream::new();
children[0].to_tokens(&mut token_stream);
token_stream
}
_ => {
let child_tokens: Vec<proc_macro2::TokenStream> = nodes_to_token_vec(children);
quote! { ::euv::VirtualNode::Fragment(vec![#(#child_tokens), *]) }
}
}
}
pub(crate) fn children_to_tokens(children: &[HtmlNode]) -> proc_macro2::TokenStream {
let child_tokens: Vec<proc_macro2::TokenStream> = nodes_to_token_vec(children);
quote! { vec![#(#child_tokens), *] }
}
pub(crate) fn parse_attr_if(content: ParseStream) -> syn::Result<HtmlAttrIf> {
let mut branches: Vec<(Option<Expr>, Expr)> = Vec::new();
content.parse::<Token![if]>()?;
let cond_content: ParseBuffer<'_>;
braced!(cond_content in content);
let condition: Expr = cond_content.parse()?;
let body_content: ParseBuffer<'_>;
braced!(body_content in content);
let body: Expr = body_content.parse()?;
branches.push((Some(condition), body));
while content.peek(Token![else]) {
content.parse::<Token![else]>()?;
if content.peek(Token![if]) {
content.parse::<Token![if]>()?;
let cond_content: ParseBuffer<'_>;
braced!(cond_content in content);
let condition: Expr = cond_content.parse()?;
let body_content: ParseBuffer<'_>;
braced!(body_content in content);
let body: Expr = body_content.parse()?;
branches.push((Some(condition), body));
} else {
let body_content: ParseBuffer<'_>;
braced!(body_content in content);
let body: Expr = body_content.parse()?;
branches.push((None, body));
break;
}
}
Ok(HtmlAttrIf { branches })
}
pub(crate) fn strip_braces_from_expr(expr: &Expr) -> &Expr {
if let Expr::Block(expr_block) = expr {
let stmts: &Vec<Stmt> = &expr_block.block.stmts;
if stmts.len() == 1
&& let Stmt::Expr(inner, None) = &stmts[0]
{
return inner;
}
}
expr
}
pub(crate) fn attr_if_to_tokens(
html_attr_if: &HtmlAttrIf,
else_default: proc_macro2::TokenStream,
) -> proc_macro2::TokenStream {
let mut if_chain: proc_macro2::TokenStream = proc_macro2::TokenStream::new();
let has_else: bool = html_attr_if
.branches
.last()
.is_some_and(|(condition, _): &(Option<Expr>, Expr)| condition.is_none());
for (branch_index, (condition, body)) in html_attr_if.branches.iter().enumerate() {
match (branch_index, condition) {
(0, Some(cond)) => {
if_chain.extend(quote! { if #cond { #body } });
}
(_, Some(cond)) => {
if_chain.extend(quote! { else if #cond { #body } });
}
(_, None) => {
if_chain.extend(quote! { else { #body } });
}
}
}
if !has_else {
if_chain.extend(quote! { else { #else_default } });
}
if_chain
}
pub(crate) fn build_html_if_chain(
branches: &[(Option<Expr>, Vec<HtmlNode>)],
) -> proc_macro2::TokenStream {
let mut if_chain: proc_macro2::TokenStream = proc_macro2::TokenStream::new();
let has_else: bool = branches
.last()
.is_some_and(|(condition, _): &(Option<Expr>, Vec<HtmlNode>)| condition.is_none());
for (branch_index, (condition, body)) in branches.iter().enumerate() {
let body_expr: proc_macro2::TokenStream = children_to_node_tokens(body);
match (branch_index, condition) {
(0, Some(cond)) => {
if_chain.extend(quote! { if #cond { #body_expr } });
}
(_, Some(cond)) => {
if_chain.extend(quote! { else if #cond { #body_expr } });
}
(_, None) => {
if_chain.extend(quote! { else { #body_expr } });
}
}
}
if !has_else {
if_chain.extend(quote! { else { ::euv::VirtualNode::Empty } });
}
if_chain
}
pub(crate) fn parse_attr_value(content: ParseStream, key_str: &str) -> syn::Result<HtmlAttrValue> {
if content.peek(Token![if]) {
return Ok(HtmlAttrValue::If(parse_attr_if(content)?));
}
if key_str == ATTR_KEY_STYLE && content.peek(Brace) {
let style_content: ParseBuffer<'_>;
braced!(style_content in content);
let is_style_object: bool = style_content.peek(LitStr) || style_content.peek(Ident);
if is_style_object {
let mut style_props: Vec<(String, HtmlStylePropValue)> = Vec::new();
while !style_content.is_empty() {
let css_key: String = parse_kebab_name(&style_content)?;
style_content.parse::<Colon>()?;
let prop_value: HtmlStylePropValue = if style_content.peek(Token![if]) {
let html_attr_if: HtmlAttrIf = parse_attr_if(&style_content)?;
HtmlStylePropValue::If(html_attr_if)
} else if style_content.peek(LitStr) {
let literal_string: LitStr = style_content.parse()?;
HtmlStylePropValue::Literal(literal_string.value())
} else if style_content.peek(Brace) {
let expr_content: ParseBuffer<'_>;
braced!(expr_content in style_content);
if expr_content.peek(Token![if]) {
let html_attr_if: HtmlAttrIf = parse_attr_if(&expr_content)?;
HtmlStylePropValue::If(html_attr_if)
} else {
let expr: Expr = expr_content.parse()?;
HtmlStylePropValue::Expr(expr)
}
} else {
let expr: Expr = style_content.parse()?;
HtmlStylePropValue::Expr(expr)
};
style_props.push((css_key, prop_value));
if style_content.peek(Semi) {
style_content.parse::<Semi>()?;
}
}
Ok(HtmlAttrValue::Style(style_props))
} else {
Ok(HtmlAttrValue::Expr(style_content.parse()?))
}
} else {
Ok(HtmlAttrValue::Expr(content.parse()?))
}
}
pub(crate) fn merge_same_key_attributes(attributes: HtmlAttrs) -> HtmlAttrs {
let mut class_values: Vec<HtmlAttrValue> = Vec::new();
let mut style_values: Vec<HtmlAttrValue> = Vec::new();
let mut result: HtmlAttrs = Vec::new();
for (key, value) in attributes {
let key_string: String = extract_attr_key_string(&key);
if key_string == ATTR_KEY_CLASS {
class_values.push(value);
} else if key_string == ATTR_KEY_STYLE {
style_values.push(value);
} else {
result.push((key, value));
}
}
let push_merged = |result: &mut HtmlAttrs,
key_str: &str,
mut values: Vec<HtmlAttrValue>,
wrap: fn(Vec<HtmlAttrValue>) -> HtmlAttrValue|
-> () {
match values.len() {
0 => {}
1 => result.push((
LitStr::new(key_str, proc_macro2::Span::call_site()).to_token_stream(),
values.remove(0),
)),
_ => result.push((
LitStr::new(key_str, proc_macro2::Span::call_site()).to_token_stream(),
wrap(values),
)),
}
};
push_merged(
&mut result,
ATTR_KEY_CLASS,
class_values,
HtmlAttrValue::Classes,
);
push_merged(
&mut result,
ATTR_KEY_STYLE,
style_values,
HtmlAttrValue::Styles,
);
result
}
pub(crate) fn attr_value_to_attribute_value_tokens(
value: &HtmlAttrValue,
key_str: &str,
is_component: bool,
) -> proc_macro2::TokenStream {
match value {
HtmlAttrValue::Expr(expr) => {
if let Some(event_name_str) = key_str.strip_prefix(EVENT_ATTR_PREFIX) {
if is_component {
quote! {
::euv::AttrValueAdapter::new(#expr).into_callback_attribute_value_with_name(#key_str)
}
} else {
quote! {
::euv::EventAdapter::new(#expr).into_attribute(#event_name_str)
}
}
} else if key_str == ATTR_KEY_CHILDREN {
quote! { ::euv::AttributeValue::Dynamic(Box::new(#expr)) }
} else {
quote! {
::euv::AttrValueAdapter::new(#expr).into_reactive_attribute_value()
}
}
}
HtmlAttrValue::If(_) => {
quote! { #value }
}
HtmlAttrValue::Style(props) => {
let has_if: bool =
props
.iter()
.any(|(_, style_value): &(String, HtmlStylePropValue)| {
matches!(style_value, HtmlStylePropValue::If(_))
});
if has_if {
quote! { #value }
} else {
quote! { ::euv::AttributeValue::Text(#value) }
}
}
HtmlAttrValue::Classes(_) | HtmlAttrValue::Styles(_) => {
quote! { #value }
}
}
}
pub(crate) fn style_value_to_attribute_value_tokens(
value: &HtmlAttrValue,
) -> proc_macro2::TokenStream {
match value {
HtmlAttrValue::Style(props) => {
let has_if: bool =
props
.iter()
.any(|(_, style_value): &(String, HtmlStylePropValue)| {
matches!(style_value, HtmlStylePropValue::If(_))
});
if has_if {
quote! { #value }
} else {
quote! { ::euv::AttributeValue::Text(#value) }
}
}
HtmlAttrValue::If(_) => {
quote! { #value }
}
HtmlAttrValue::Expr(expr) => {
quote! { ::euv::AttributeValue::Text(#expr.to_string()) }
}
HtmlAttrValue::Classes(_) | HtmlAttrValue::Styles(_) => {
quote! { #value }
}
}
}
pub(crate) fn extract_attr_key_string(key: &proc_macro2::TokenStream) -> String {
let raw: String = key.to_string().replace(CHAR_SPACE, STR_EMPTY);
if raw.starts_with(CHAR_DOUBLE_QUOTE) && raw.ends_with(CHAR_DOUBLE_QUOTE) {
raw[1..raw.len() - 1].to_string()
} else {
raw.strip_prefix(RAW_IDENT_PREFIX)
.unwrap_or(&raw)
.to_string()
}
}