use proc_macro::TokenStream;
use proc_macro2::TokenTree;
use quote::quote;
use syn::{parse::Parse, parse::ParseStream, LitStr};
#[proc_macro]
pub fn lino(input: TokenStream) -> TokenStream {
let input2: proc_macro2::TokenStream = input.into();
let lino_str = match syn::parse2::<LitStr>(input2.clone()) {
Ok(lit_str) => lit_str.value(),
Err(_) => {
match syn::parse2::<DirectLinoInput>(input2.clone()) {
Ok(direct) => direct.content,
Err(e) => {
return syn::Error::new(
proc_macro2::Span::call_site(),
format!("Failed to parse Links Notation input: {}", e),
)
.to_compile_error()
.into();
}
}
}
};
if let Err(e) = validate_lino_syntax(&lino_str) {
return syn::Error::new(
proc_macro2::Span::call_site(),
format!("Invalid Links Notation: {}", e),
)
.to_compile_error()
.into();
}
let expanded = quote! {
{
const _: () = {
let _ = #lino_str;
};
links_notation::parse_lino(#lino_str).expect("lino! macro: validated at compile time but runtime parse failed")
}
};
TokenStream::from(expanded)
}
struct DirectLinoInput {
content: String,
}
impl Parse for DirectLinoInput {
fn parse(input: ParseStream) -> syn::Result<Self> {
let mut content = String::new();
let tokens: proc_macro2::TokenStream = input.parse()?;
tokens_to_lino_string(tokens, &mut content);
Ok(DirectLinoInput { content })
}
}
fn tokens_to_lino_string(tokens: proc_macro2::TokenStream, output: &mut String) {
let mut prev_needs_space = false;
let mut tokens_iter = tokens.into_iter().peekable();
while let Some(token) = tokens_iter.next() {
match token {
TokenTree::Ident(ident) => {
if prev_needs_space {
output.push(' ');
}
output.push_str(&ident.to_string());
prev_needs_space = true;
}
TokenTree::Punct(punct) => {
let ch = punct.as_char();
match ch {
':' => {
output.push(':');
prev_needs_space = true;
}
'-' => {
if let Some(TokenTree::Literal(_) | TokenTree::Ident(_)) =
tokens_iter.peek()
{
if prev_needs_space {
output.push(' ');
}
output.push('-');
prev_needs_space = false;
} else {
if prev_needs_space {
output.push(' ');
}
output.push('-');
prev_needs_space = true;
}
}
'_' => {
output.push('_');
prev_needs_space = false;
}
'.' => {
output.push('.');
prev_needs_space = false;
}
'\'' => {
output.push('\'');
prev_needs_space = false;
}
'"' => {
output.push('"');
prev_needs_space = false;
}
_ => {
if prev_needs_space && !matches!(ch, ',' | ';' | '!' | '?') {
output.push(' ');
}
output.push(ch);
prev_needs_space = !matches!(ch, '(' | '[' | '{' | '<');
}
}
}
TokenTree::Literal(lit) => {
if prev_needs_space {
output.push(' ');
}
let lit_str = lit.to_string();
if (lit_str.starts_with('"') && lit_str.ends_with('"'))
|| (lit_str.starts_with('\'') && lit_str.ends_with('\''))
{
output.push_str(&lit_str);
} else {
output.push_str(&lit_str);
}
prev_needs_space = true;
}
TokenTree::Group(group) => {
let delimiter = group.delimiter();
match delimiter {
proc_macro2::Delimiter::Parenthesis => {
if prev_needs_space {
output.push(' ');
}
output.push('(');
tokens_to_lino_string(group.stream(), output);
output.push(')');
prev_needs_space = true;
}
proc_macro2::Delimiter::Bracket => {
if prev_needs_space {
output.push(' ');
}
output.push('[');
tokens_to_lino_string(group.stream(), output);
output.push(']');
prev_needs_space = true;
}
proc_macro2::Delimiter::Brace => {
if prev_needs_space {
output.push(' ');
}
output.push('{');
tokens_to_lino_string(group.stream(), output);
output.push('}');
prev_needs_space = true;
}
proc_macro2::Delimiter::None => {
tokens_to_lino_string(group.stream(), output);
}
}
}
}
}
}
fn validate_lino_syntax(input: &str) -> Result<(), String> {
let mut depth = 0;
let mut in_single_quote = false;
let mut in_double_quote = false;
let mut escape_next = false;
for c in input.chars() {
if escape_next {
escape_next = false;
continue;
}
match c {
'\\' => escape_next = true,
'\'' if !in_double_quote => in_single_quote = !in_single_quote,
'"' if !in_single_quote => in_double_quote = !in_double_quote,
'(' if !in_single_quote && !in_double_quote => depth += 1,
')' if !in_single_quote && !in_double_quote => {
depth -= 1;
if depth < 0 {
return Err("Unmatched closing parenthesis".to_string());
}
}
_ => {}
}
}
if depth != 0 {
return Err(format!(
"Unbalanced parentheses: {} unclosed opening parenthes{}",
depth,
if depth == 1 { "is" } else { "es" }
));
}
if in_single_quote {
return Err("Unclosed single quote".to_string());
}
if in_double_quote {
return Err("Unclosed double quote".to_string());
}
Ok(())
}
#[cfg(test)]
mod tests;