use quote::quote;
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::{Data, DeriveInput, Fields, Ident, LitBool, LitStr, Token};
use crate::shared::{box_style_tokens, named_field_ident, snake_to_title_case};
#[derive(Default)]
pub(crate) struct PanelAttrs {
title: Option<LitStr>,
subtitle: Option<LitStr>,
box_style: Option<LitStr>,
border_style: Option<LitStr>,
style: Option<LitStr>,
title_style: Option<LitStr>,
expand: Option<LitBool>,
highlight: Option<LitBool>,
}
pub(crate) struct PanelAttr {
pub(crate) key: Ident,
pub(crate) value: PanelAttrValue,
}
pub(crate) enum PanelAttrValue {
Str(LitStr),
Bool(LitBool),
Flag,
}
impl Parse for PanelAttr {
fn parse(input: ParseStream) -> syn::Result<Self> {
let key: Ident = input.parse()?;
if input.peek(Token![=]) {
let _eq: Token![=] = input.parse()?;
if input.peek(LitStr) {
let lit: LitStr = input.parse()?;
Ok(PanelAttr {
key,
value: PanelAttrValue::Str(lit),
})
} else if input.peek(LitBool) {
let lit: LitBool = input.parse()?;
Ok(PanelAttr {
key,
value: PanelAttrValue::Bool(lit),
})
} else {
Err(input.error("expected string literal or bool"))
}
} else {
Ok(PanelAttr {
key,
value: PanelAttrValue::Flag,
})
}
}
}
pub(crate) fn parse_panel_attrs(input: &DeriveInput) -> syn::Result<PanelAttrs> {
let mut attrs = PanelAttrs::default();
for attr in &input.attrs {
if !attr.path().is_ident("panel") {
continue;
}
let items: Punctuated<PanelAttr, Token![,]> =
attr.parse_args_with(Punctuated::parse_terminated)?;
for item in items {
let key_str = item.key.to_string();
match key_str.as_str() {
"title" => {
attrs.title = Some(panel_expect_str(&item, "title")?);
}
"subtitle" => {
attrs.subtitle = Some(panel_expect_str(&item, "subtitle")?);
}
"box_style" => {
attrs.box_style = Some(panel_expect_str(&item, "box_style")?);
}
"border_style" => {
attrs.border_style = Some(panel_expect_str(&item, "border_style")?);
}
"style" => {
attrs.style = Some(panel_expect_str(&item, "style")?);
}
"title_style" => {
attrs.title_style = Some(panel_expect_str(&item, "title_style")?);
}
"expand" => {
attrs.expand = Some(panel_expect_bool(&item, "expand")?);
}
"highlight" => {
attrs.highlight = Some(panel_expect_bool(&item, "highlight")?);
}
_ => {
return Err(syn::Error::new_spanned(
&item.key,
format!("unknown panel attribute `{}`", key_str),
));
}
}
}
}
Ok(attrs)
}
pub(crate) fn panel_expect_str(attr: &PanelAttr, name: &str) -> syn::Result<LitStr> {
match &attr.value {
PanelAttrValue::Str(s) => Ok(s.clone()),
_ => Err(syn::Error::new_spanned(
&attr.key,
format!("`{}` expects a string literal", name),
)),
}
}
pub(crate) fn panel_expect_bool(attr: &PanelAttr, _name: &str) -> syn::Result<LitBool> {
match &attr.value {
PanelAttrValue::Bool(b) => Ok(b.clone()),
PanelAttrValue::Flag => Ok(LitBool::new(true, attr.key.span())),
_ => Err(syn::Error::new_spanned(
&attr.key,
format!("`{}` expects a bool", _name),
)),
}
}
#[derive(Default)]
pub(crate) struct FieldAttrs {
pub(crate) label: Option<LitStr>,
pub(crate) style: Option<LitStr>,
pub(crate) skip: Option<LitBool>,
}
pub(crate) struct FieldAttr {
pub(crate) key: Ident,
pub(crate) value: FieldAttrValue,
}
pub(crate) enum FieldAttrValue {
Str(LitStr),
Bool(LitBool),
Flag,
}
impl Parse for FieldAttr {
fn parse(input: ParseStream) -> syn::Result<Self> {
let key: Ident = input.parse()?;
if input.peek(Token![=]) {
let _eq: Token![=] = input.parse()?;
if input.peek(LitStr) {
let lit: LitStr = input.parse()?;
Ok(FieldAttr {
key,
value: FieldAttrValue::Str(lit),
})
} else if input.peek(LitBool) {
let lit: LitBool = input.parse()?;
Ok(FieldAttr {
key,
value: FieldAttrValue::Bool(lit),
})
} else {
Err(input.error("expected string literal or bool"))
}
} else {
Ok(FieldAttr {
key,
value: FieldAttrValue::Flag,
})
}
}
}
pub(crate) fn parse_field_attrs(field: &syn::Field) -> syn::Result<FieldAttrs> {
let mut attrs = FieldAttrs::default();
for attr in &field.attrs {
if !attr.path().is_ident("field") {
continue;
}
let items: Punctuated<FieldAttr, Token![,]> =
attr.parse_args_with(Punctuated::parse_terminated)?;
for item in items {
let key_str = item.key.to_string();
match key_str.as_str() {
"label" => {
attrs.label = Some(field_expect_str(&item, "label")?);
}
"style" => {
attrs.style = Some(field_expect_str(&item, "style")?);
}
"skip" => {
attrs.skip = Some(field_expect_bool(&item, "skip")?);
}
_ => {
return Err(syn::Error::new_spanned(
&item.key,
format!("unknown field attribute `{}`", key_str),
));
}
}
}
}
Ok(attrs)
}
pub(crate) fn field_expect_str(attr: &FieldAttr, name: &str) -> syn::Result<LitStr> {
match &attr.value {
FieldAttrValue::Str(s) => Ok(s.clone()),
_ => Err(syn::Error::new_spanned(
&attr.key,
format!("`{}` expects a string literal", name),
)),
}
}
pub(crate) fn field_expect_bool(attr: &FieldAttr, _name: &str) -> syn::Result<LitBool> {
match &attr.value {
FieldAttrValue::Bool(b) => Ok(b.clone()),
FieldAttrValue::Flag => Ok(LitBool::new(true, attr.key.span())),
_ => Err(syn::Error::new_spanned(
&attr.key,
format!("`{}` expects a bool", _name),
)),
}
}
pub(crate) fn derive_panel_impl(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
let struct_name = &input.ident;
let struct_name_str = struct_name.to_string();
let fields = match &input.data {
Data::Struct(data_struct) => match &data_struct.fields {
Fields::Named(named) => &named.named,
Fields::Unnamed(_) => {
return Err(syn::Error::new_spanned(
struct_name,
"Panel derive only supports structs with named fields",
));
}
Fields::Unit => {
return Err(syn::Error::new_spanned(
struct_name,
"Panel derive does not support unit structs",
));
}
},
Data::Enum(_) => {
return Err(syn::Error::new_spanned(
struct_name,
"Panel derive does not support enums",
));
}
Data::Union(_) => {
return Err(syn::Error::new_spanned(
struct_name,
"Panel derive does not support unions",
));
}
};
let panel_attrs = parse_panel_attrs(input)?;
struct PanelFieldInfo {
ident: Ident,
label: String,
style: Option<String>,
}
let mut field_infos: Vec<PanelFieldInfo> = Vec::new();
for field in fields.iter() {
let ident = named_field_ident(field)?.clone();
let fa = parse_field_attrs(field)?;
let skip = fa.skip.as_ref().map(|b| b.value).unwrap_or(false);
if skip {
continue;
}
let label = match &fa.label {
Some(lit) => lit.value(),
None => snake_to_title_case(&ident.to_string()),
};
let style = fa.style.as_ref().map(|lit| lit.value());
field_infos.push(PanelFieldInfo {
ident,
label,
style,
});
}
let line_pushes: Vec<proc_macro2::TokenStream> = field_infos
.iter()
.map(|fi| {
let ident = &fi.ident;
let label = &fi.label;
match &fi.style {
Some(sty) => {
let open_tag = format!("[{}]", sty);
let close_tag = format!("[/{}]", sty);
quote! {
lines.push(format!("{}{}:{} {}", #open_tag, #label, #close_tag, self.#ident));
}
}
None => {
quote! {
lines.push(format!("{}: {}", #label, self.#ident));
}
}
}
})
.collect();
let title_value = match &panel_attrs.title {
Some(lit) => lit.value(),
None => struct_name_str.clone(),
};
let mut panel_config = Vec::new();
if let Some(ref lit) = panel_attrs.title_style {
let sty = lit.value();
let styled_title = format!("[{}]{}[/{}]", sty, title_value, sty);
panel_config.push(quote! {
panel.title = Some(gilt::text::Text::from_markup(#styled_title).unwrap_or_else(|_| gilt::text::Text::from(#title_value)));
});
} else {
panel_config.push(quote! {
panel.title = Some(gilt::text::Text::from(#title_value));
});
}
if let Some(ref lit) = panel_attrs.subtitle {
let val = lit.value();
panel_config.push(quote! {
panel.subtitle = Some(gilt::text::Text::from(#val));
});
}
if let Some(ref lit) = panel_attrs.box_style {
let tokens = box_style_tokens(lit)?;
panel_config.push(quote! {
if let Some(bc) = #tokens {
panel.box_chars = bc;
}
});
}
if let Some(ref lit) = panel_attrs.border_style {
let val = lit.value();
panel_config.push(quote! {
panel.border_style = gilt::style::Style::parse(#val).unwrap_or_else(|_| gilt::style::Style::null());
});
}
if let Some(ref lit) = panel_attrs.style {
let val = lit.value();
panel_config.push(quote! {
panel.style = gilt::style::Style::parse(#val).unwrap_or_else(|_| gilt::style::Style::null());
});
}
if let Some(ref lit) = panel_attrs.expand {
let val = lit.value;
panel_config.push(quote! {
panel.expand = #val;
});
}
if let Some(ref lit) = panel_attrs.highlight {
let val = lit.value;
panel_config.push(quote! {
panel.highlight = #val;
});
}
let expanded = quote! {
impl #struct_name {
pub fn to_panel(&self) -> gilt::panel::Panel {
let mut lines: Vec<String> = Vec::new();
#(#line_pushes)*
let content = gilt::text::Text::from_markup(&lines.join("\n"))
.unwrap_or_else(|_| gilt::text::Text::from(lines.join("\n").as_str()));
let mut panel = gilt::panel::Panel::new(content);
#(#panel_config)*
panel
}
}
};
Ok(expanded)
}