use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use std::sync::atomic::{AtomicU64, Ordering};
use syn::{
braced,
parse::{Parse, ParseStream},
parse_macro_input,
punctuated::Punctuated,
Expr, Ident, LitStr, Result, Token,
};
#[proc_macro]
pub fn view(input: TokenStream) -> TokenStream {
let node = parse_macro_input!(input as ViewNode);
let expanded = node.to_tokens();
TokenStream::from(expanded)
}
struct WithInput {
idents: Vec<Ident>,
expr: Expr,
}
impl Parse for WithInput {
fn parse(input: ParseStream) -> Result<Self> {
let idents: Punctuated<Ident, Token![,]> = Punctuated::parse_separated_nonempty(input)?;
let idents: Vec<Ident> = idents.into_iter().collect();
input.parse::<Token![=>]>()?;
let expr: Expr = input.parse()?;
Ok(WithInput { idents, expr })
}
}
impl WithInput {
fn to_tokens(&self) -> TokenStream2 {
let clone_statements: Vec<TokenStream2> = self
.idents
.iter()
.map(|ident| quote! { let #ident = #ident.clone(); })
.collect();
let expr = &self.expr;
quote! {
{
#(#clone_statements)*
#expr
}
}
}
}
static STATE_COUNTER: AtomicU64 = AtomicU64::new(0);
struct StateInput {
scope: Expr,
init: Expr,
}
impl Parse for StateInput {
fn parse(input: ParseStream) -> Result<Self> {
let scope: Expr = input.parse()?;
input.parse::<Token![,]>()?;
let init: Expr = input.parse()?;
Ok(StateInput { scope, init })
}
}
impl StateInput {
fn to_tokens(&self) -> TokenStream2 {
let scope = &self.scope;
let init = &self.init;
let counter = STATE_COUNTER.fetch_add(1, Ordering::SeqCst);
let key_type = format_ident!("__State_{}", counter);
quote! {
{
struct #key_type;
#scope.use_state_keyed::<#key_type, _>(#init)
}
}
}
}
#[proc_macro]
pub fn state(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as StateInput);
let expanded = input.to_tokens();
TokenStream::from(expanded)
}
static EFFECT_COUNTER: AtomicU64 = AtomicU64::new(0);
struct EffectInput {
scope: Expr,
deps: Expr,
effect_fn: Expr,
}
impl Parse for EffectInput {
fn parse(input: ParseStream) -> Result<Self> {
let scope: Expr = input.parse()?;
input.parse::<Token![,]>()?;
let deps: Expr = input.parse()?;
input.parse::<Token![,]>()?;
let effect_fn: Expr = input.parse()?;
Ok(EffectInput {
scope,
deps,
effect_fn,
})
}
}
impl EffectInput {
fn to_tokens(&self) -> TokenStream2 {
let scope = &self.scope;
let deps = &self.deps;
let effect_fn = &self.effect_fn;
let counter = EFFECT_COUNTER.fetch_add(1, Ordering::SeqCst);
let key_type = format_ident!("__Effect_{}", counter);
quote! {
{
struct #key_type;
#scope.use_effect_keyed::<#key_type, _, _, _>(#deps, #effect_fn)
}
}
}
}
#[proc_macro]
pub fn effect(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as EffectInput);
let expanded = input.to_tokens();
TokenStream::from(expanded)
}
struct EffectOnceInput {
scope: Expr,
effect_fn: Expr,
}
impl Parse for EffectOnceInput {
fn parse(input: ParseStream) -> Result<Self> {
let scope: Expr = input.parse()?;
input.parse::<Token![,]>()?;
let effect_fn: Expr = input.parse()?;
Ok(EffectOnceInput { scope, effect_fn })
}
}
impl EffectOnceInput {
fn to_tokens(&self) -> TokenStream2 {
let scope = &self.scope;
let effect_fn = &self.effect_fn;
let counter = EFFECT_COUNTER.fetch_add(1, Ordering::SeqCst);
let key_type = format_ident!("__Effect_{}", counter);
quote! {
{
struct #key_type;
#scope.use_effect_once_keyed::<#key_type, _, _>(#effect_fn)
}
}
}
}
#[proc_macro]
pub fn effect_once(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as EffectOnceInput);
let expanded = input.to_tokens();
TokenStream::from(expanded)
}
#[proc_macro]
pub fn with(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as WithInput);
let expanded = input.to_tokens();
TokenStream::from(expanded)
}
enum ViewNode {
Element(ElementNode),
Text(String),
Expr(Expr),
}
struct Prop {
name: Ident,
value: Expr,
}
struct ElementNode {
tag: String,
props: Vec<Prop>,
children: Vec<ViewNode>,
}
impl Parse for ViewNode {
fn parse(input: ParseStream) -> Result<Self> {
if input.peek(Token![<]) {
input.parse::<Token![<]>()?;
let tag: Ident = input.parse()?;
let mut props = Vec::new();
while !input.peek(Token![>]) && !input.peek(Token![/]) {
let name: Ident = input.parse()?;
input.parse::<Token![=]>()?;
let content;
braced!(content in input);
let value: Expr = content.parse()?;
props.push(Prop { name, value });
}
if input.peek(Token![/]) {
input.parse::<Token![/]>()?;
input.parse::<Token![>]>()?;
return Ok(ViewNode::Element(ElementNode {
tag: tag.to_string(),
props,
children: Vec::new(),
}));
}
input.parse::<Token![>]>()?;
let mut children = Vec::new();
while !(input.peek(Token![<]) && input.peek2(Token![/])) {
if input.is_empty() {
return Err(syn::Error::new(
tag.span(),
format!("Unclosed tag: <{}>", tag),
));
}
children.push(input.parse()?);
}
input.parse::<Token![<]>()?;
input.parse::<Token![/]>()?;
let close_tag: Ident = input.parse()?;
input.parse::<Token![>]>()?;
if tag != close_tag {
return Err(syn::Error::new(
close_tag.span(),
format!(
"Mismatched tags: expected </{}>, found </{}>",
tag, close_tag
),
));
}
Ok(ViewNode::Element(ElementNode {
tag: tag.to_string(),
props,
children,
}))
} else if input.peek(LitStr) {
let lit: LitStr = input.parse()?;
Ok(ViewNode::Text(lit.value()))
} else if input.peek(syn::token::Brace) {
let content;
braced!(content in input);
let expr: Expr = content.parse()?;
Ok(ViewNode::Expr(expr))
} else {
Err(input.error("Expected <Element>, \"string literal\", or {expression}"))
}
}
}
impl ViewNode {
fn to_tokens(&self) -> TokenStream2 {
match self {
ViewNode::Text(s) => {
quote! { telex::View::text(#s) }
}
ViewNode::Expr(expr) => {
quote! { telex::View::text(format!("{}", #expr)) }
}
ViewNode::Element(elem) => elem.to_tokens(),
}
}
}
impl ElementNode {
fn to_tokens(&self) -> TokenStream2 {
match self.tag.as_str() {
"Text" => {
if let Some(child) = self.children.first() {
match child {
ViewNode::Text(content) => quote! { telex::View::text(#content) },
ViewNode::Expr(expr) => quote! { telex::View::text(format!("{}", #expr)) },
_ => quote! { telex::View::text("") },
}
} else {
quote! { telex::View::text("") }
}
}
"VStack" => {
let mut builder_calls = Vec::new();
for prop in &self.props {
let name = &prop.name;
let value = &prop.value;
builder_calls.push(quote! { .#name(#value) });
}
for child in &self.children {
let tokens = child.to_tokens();
builder_calls.push(quote! { .child(#tokens) });
}
quote! { telex::View::vstack()#(#builder_calls)*.build() }
}
"HStack" => {
let mut builder_calls = Vec::new();
for prop in &self.props {
let name = &prop.name;
let value = &prop.value;
builder_calls.push(quote! { .#name(#value) });
}
for child in &self.children {
let tokens = child.to_tokens();
builder_calls.push(quote! { .child(#tokens) });
}
quote! { telex::View::hstack()#(#builder_calls)*.build() }
}
"Box" => {
let mut builder_calls = Vec::new();
for prop in &self.props {
let name = &prop.name;
let value = &prop.value;
builder_calls.push(quote! { .#name(#value) });
}
if let Some(child) = self.children.first() {
let tokens = child.to_tokens();
builder_calls.push(quote! { .child(#tokens) });
}
quote! { telex::View::boxed()#(#builder_calls)*.build() }
}
"Spacer" => {
if let Some(prop) = self.props.iter().find(|p| p.name == "flex") {
let value = &prop.value;
quote! { telex::View::spacer_flex(#value) }
} else {
quote! { telex::View::spacer() }
}
}
"Button" => {
let mut builder_calls = Vec::new();
for prop in &self.props {
let name = &prop.name;
let value = &prop.value;
builder_calls.push(quote! { .#name(#value) });
}
if let Some(child) = self.children.first() {
match child {
ViewNode::Text(label) => {
builder_calls.push(quote! { .label(#label) });
}
ViewNode::Expr(expr) => {
builder_calls.push(quote! { .label(format!("{}", #expr)) });
}
_ => {}
}
}
quote! { telex::View::button()#(#builder_calls)*.build() }
}
"List" => {
let mut builder_calls = Vec::new();
for prop in &self.props {
let name = &prop.name;
let value = &prop.value;
builder_calls.push(quote! { .#name(#value) });
}
quote! { telex::View::list()#(#builder_calls)*.build() }
}
"TextInput" => {
let mut builder_calls = Vec::new();
for prop in &self.props {
let name = &prop.name;
let value = &prop.value;
builder_calls.push(quote! { .#name(#value) });
}
quote! { telex::View::text_input()#(#builder_calls)*.build() }
}
"Checkbox" => {
let mut builder_calls = Vec::new();
for prop in &self.props {
let name = &prop.name;
let value = &prop.value;
builder_calls.push(quote! { .#name(#value) });
}
if let Some(child) = self.children.first() {
match child {
ViewNode::Text(label) => {
builder_calls.push(quote! { .label(#label) });
}
ViewNode::Expr(expr) => {
builder_calls.push(quote! { .label(format!("{}", #expr)) });
}
_ => {}
}
}
quote! { telex::View::checkbox()#(#builder_calls)*.build() }
}
"TextArea" => {
let mut builder_calls = Vec::new();
for prop in &self.props {
let name = &prop.name;
let value = &prop.value;
builder_calls.push(quote! { .#name(#value) });
}
quote! { telex::View::text_area()#(#builder_calls)*.build() }
}
"Modal" => {
let mut builder_calls = Vec::new();
for prop in &self.props {
let name = &prop.name;
let value = &prop.value;
builder_calls.push(quote! { .#name(#value) });
}
if let Some(child) = self.children.first() {
let tokens = child.to_tokens();
builder_calls.push(quote! { .child(#tokens) });
}
quote! { telex::View::modal()#(#builder_calls)*.build() }
}
"StyledText" => {
let mut content = quote! { "" };
let mut bold_val = quote! { false };
let mut italic_val = quote! { false };
let mut underline_val = quote! { false };
let mut dim_val = quote! { false };
let mut color_call = quote! {};
let mut bg_call = quote! {};
for prop in &self.props {
let name_str = prop.name.to_string();
let value = &prop.value;
match name_str.as_str() {
"bold" => bold_val = quote! { #value },
"italic" => italic_val = quote! { #value },
"underline" => underline_val = quote! { #value },
"dim" => dim_val = quote! { #value },
"color" => color_call = quote! { .color(#value) },
"bg" => bg_call = quote! { .bg(#value) },
_ => {}
}
}
if let Some(child) = self.children.first() {
match child {
ViewNode::Text(text) => {
content = quote! { #text };
}
ViewNode::Expr(expr) => {
content = quote! { format!("{}", #expr) };
}
_ => {}
}
}
quote! {
{
let __builder = telex::View::styled_text(#content);
let __builder = if #bold_val { __builder.bold() } else { __builder };
let __builder = if #italic_val { __builder.italic() } else { __builder };
let __builder = if #underline_val { __builder.underline() } else { __builder };
let __builder = if #dim_val { __builder.dim() } else { __builder };
__builder #color_call #bg_call .build()
}
}
}
unknown => {
let known_elements = [
"Text",
"StyledText",
"VStack",
"HStack",
"Box",
"Spacer",
"Button",
"List",
"TextInput",
"TextArea",
"Checkbox",
"Modal",
];
let suggestion = known_elements
.iter()
.find(|&e| {
let e_lower = e.to_lowercase();
let u_lower = unknown.to_lowercase();
e_lower.starts_with(&u_lower[..1.min(u_lower.len())])
|| u_lower.starts_with(&e_lower[..1.min(e_lower.len())])
|| e_lower.contains(&u_lower)
|| u_lower.contains(&e_lower)
});
let msg = if let Some(suggested) = suggestion {
format!(
"Unknown element: <{}>. Did you mean <{}>?\n\nAvailable elements: {}",
unknown,
suggested,
known_elements.join(", ")
)
} else {
format!(
"Unknown element: <{}>.\n\nAvailable elements: {}",
unknown,
known_elements.join(", ")
)
};
quote! { compile_error!(#msg) }
}
}
}
}