use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::{Ident, Visibility};
use crate::ast::capture::{ExampleItem, FieldDef};
type CaptureInit = TokenStream;
type StructDef = TokenStream;
type StructExpr = TokenStream;
type CaptureList = Vec<Ident>;
pub fn generate_output(
capture_list: &[FieldDef],
ident: Option<Ident>,
visibility: Option<Visibility>,
) -> (CaptureInit, StructDef, StructExpr, CaptureList) {
let ident = ident.unwrap_or_else(|| format_ident!("Output"));
let visibility = visibility.unwrap_or(Visibility::Inherited);
let mut capture_init = TokenStream::new();
let is_inline = capture_list.first().map(|f| f.is_inline).unwrap_or(false);
capture_init.extend(capture_list.iter().map(
|FieldDef {
name,
ty,
is_optional,
..
}| {
if *is_optional {
quote! {
#[allow(unused)]
let mut #name: #ty = ::std::option::Option::None;
}
} else {
quote! {
let #name: #ty;
}
}
},
));
let mut struct_fields = TokenStream::new();
struct_fields.extend(capture_list.iter().map(
|FieldDef {
name,
ty,
is_inline,
..
}| {
if *is_inline {
quote! { #ty, }
} else {
quote! { pub #name: #ty,}
}
},
));
let mut struct_expr_fields = TokenStream::new();
let capture_ident_list: Vec<Ident> = capture_list
.iter()
.map(|FieldDef { name, .. }| name.clone())
.collect();
struct_expr_fields.extend(capture_ident_list.iter().map(|ident| {
quote! {#ident,}
}));
if is_inline {
(
capture_init,
quote! { #visibility type #ident = (#struct_fields); },
quote! { (#struct_expr_fields) },
capture_ident_list,
)
} else {
(
capture_init,
quote! { #visibility struct #ident { #struct_fields } },
quote! { #ident { #struct_expr_fields } },
capture_ident_list,
)
}
}
pub fn generate_example(
example_items: &[ExampleItem],
is_block: bool,
is_extra: bool,
is_inline: bool,
) -> (String, Vec<String>) {
let mut example = String::new();
let mut extra_example = vec![];
let last_index = example_items.len() - 1;
for (i, item) in example_items.iter().enumerate() {
example += &match item {
ExampleItem::Literal(lit) => {
if is_extra {
format!("{} ", lit)
} else {
format!("**{}** ", lit)
}
}
ExampleItem::Block {
optional,
example,
iter,
} => {
let (inner, mut extra) = generate_example(example, true, is_extra, false);
extra_example.append(&mut extra);
let suffix = format!(
"{}{}",
if iter.is_empty() {
String::new()
} else {
format!("{} ...", iter)
},
if *optional { "?" } else { "" }
);
if is_extra {
format!("{}\n\n{}{}\n\n{}", "```ignore", inner, suffix, "```")
} else if is_block {
format!(" {}{} ", inner, suffix)
} else {
format!("*[{}{}]()* ", inner, suffix)
}
}
ExampleItem::Capture { name, ty } => {
let ty = ty.replace(" ", ""); if name.is_empty() {
if is_extra {
ty.to_string()
} else {
format!("*{}* ", ty)
}
} else if is_extra {
if is_inline {
format!("{}@{},", name, ty)
} else {
format!(" {}@{},", name, ty)
}
} else if is_block {
format!("*{}@`{}`* ", name, ty)
} else {
format!("*[{}@`{}`]()* ", name, ty)
}
}
ExampleItem::Poly {
name,
syntex_name,
example,
} => {
let (inner, mut extra) = generate_example(example, is_block, true, false);
extra_example.push(format!(
"```ignore\n{} {{\n{}\n}}\n```\n",
syntex_name, inner
));
extra_example.append(&mut extra);
if is_extra {
syntex_name.to_string()
} else if name.is_empty() {
format!("*{}* ", syntex_name)
} else {
format!("*{}@`{}`* ", name, syntex_name)
}
}
ExampleItem::Group { delimiter, example } => {
let (inner, mut extra) = generate_example(example, is_block, is_extra, is_extra);
extra_example.append(&mut extra);
if is_extra {
if delimiter.0.is_empty() {
if is_inline {
inner
} else {
format!(" {inner}")
}
} else if is_inline {
format!("{}{}{}", delimiter.0, inner, delimiter.1)
} else {
format!("{}\n {}\n{}", delimiter.0, inner, delimiter.1)
}
} else if delimiter.0.is_empty() {
format!("{} ", inner)
} else {
format!(
"**{}**\n\n {}\n\n**{}** ",
delimiter.0, inner, delimiter.1
)
}
}
};
example += if !is_inline && is_extra && last_index != i {
"\n"
} else {
""
};
}
(example, extra_example)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::capture::ExampleItem;
use proc_macro2::Span;
use syn::{parse_quote, token::Pub};
fn field(name: &str, ty: &str, is_optional: bool, is_inline: bool) -> FieldDef {
FieldDef {
name: syn::parse_str(name).unwrap(),
ty: syn::parse_str(ty).unwrap(),
is_optional,
is_inline,
}
}
#[test]
fn test_generate_struct_fields_comma() {
let fields = vec![
field("name", "Ident", false, false),
field("ret", "Type", false, false),
];
let (_, struct_def, _, _) = generate_output(&fields, Some(parse_quote!(MyStruct)), None);
let output = struct_def.to_string();
assert!(output.contains("struct MyStruct"));
assert!(output.contains("name : Ident ,")); assert!(output.contains("ret : Type ,"));
let (_, struct_def, _, _) = generate_output(
&fields,
Some(parse_quote!(MyStruct)),
Some(Visibility::Public(Pub {
span: Span::call_site(),
})),
);
let output = struct_def.to_string();
assert!(output.contains("pub struct MyStruct"));
let (_, struct_def, _, _) = generate_output(
&fields,
Some(parse_quote!(MyStruct)),
Some(parse_quote! {pub(in a::b::c)}),
);
let output = struct_def.to_string();
assert!(output.contains("pub (in a :: b :: c) struct MyStruct"));
}
#[test]
fn test_generate_inline_tuple_type() {
let fields = vec![
field("_0", "Ident", false, true),
field("_1", "Type", false, true),
];
let (_, struct_def, _, _) = generate_output(&fields, Some(parse_quote!(MyTuple)), None);
let output = struct_def.to_string();
assert!(output.contains("type MyTuple ="));
assert!(output.contains("(Ident , Type ,)"));
assert!(!output.contains("_0"));
}
#[test]
fn test_generate_optional_fields() {
let fields = vec![
field("opt_val", "u32", true, false), ];
let (capture_init, _, _, _) = generate_output(&fields, None, None);
let init_code = capture_init.to_string();
assert!(init_code.contains("let mut opt_val : u32 ="));
assert!(init_code.contains("Option :: None"));
}
#[test]
fn test_generate_empty_capture() {
let fields = vec![];
let (_, struct_def, struct_expr, _) =
generate_output(&fields, Some(parse_quote!(MyEmpty)), None);
let def_str = struct_def.to_string();
let expr_str = struct_expr.to_string();
assert!(def_str.contains("struct MyEmpty"));
assert!(!def_str.contains(", }"));
assert!(expr_str.contains("MyEmpty"));
}
#[test]
fn test_generate_example_marks_extra_blocks_as_ignored() {
let items = vec![ExampleItem::Block {
optional: false,
example: vec![ExampleItem::Capture {
name: "item".to_string(),
ty: "Ident".to_string(),
}],
iter: String::new(),
}];
let (example, extra) = generate_example(&items, false, true, false);
assert!(example.starts_with("```ignore"));
assert!(!example.starts_with("```\n"));
assert!(extra.is_empty());
}
#[test]
fn test_generate_example_marks_poly_examples_as_ignored() {
let items = vec![ExampleItem::Poly {
name: "polymorphic".to_string(),
syntex_name: "Polymorphic".to_string(),
example: vec![ExampleItem::Capture {
name: "LitInt".to_string(),
ty: "syn::LitInt".to_string(),
}],
}];
let (_example, extra) = generate_example(&items, false, false, false);
assert_eq!(extra.len(), 1);
assert!(extra[0].starts_with("```ignore\nPolymorphic {"));
assert!(extra[0].contains("LitInt@syn::LitInt,"));
assert!(!extra[0].starts_with("```\n"));
}
}