extern crate proc_macro;
use core::str::CharIndices;
use proc_macro::TokenStream;
use regex::Regex;
use std::ops::Range;
#[proc_macro]
pub fn ex_format(item: TokenStream) -> TokenStream {
ex_impl("format", &item.to_string()).parse().unwrap()
}
#[proc_macro]
pub fn ex_print(item: TokenStream) -> TokenStream {
ex_impl("print", &item.to_string()).parse().unwrap()
}
#[proc_macro]
pub fn ex_println(item: TokenStream) -> TokenStream {
ex_impl("println", &item.to_string()).parse().unwrap()
}
#[proc_macro]
pub fn ex_eprint(item: TokenStream) -> TokenStream {
ex_impl("eprint", &item.to_string()).parse().unwrap()
}
#[proc_macro]
pub fn ex_eprintln(item: TokenStream) -> TokenStream {
ex_impl("eprintln", &item.to_string()).parse().unwrap()
}
fn ex_impl(func: &str, arg: &str) -> String {
let mod_regex = Regex::new(r#"^:(?:[^\?\s\$\*]*[\?|\s])"#).unwrap();
let mut ex_fmt = String::with_capacity(arg.len());
let mut ex_args = String::with_capacity(arg.len());
let mut search_index = 0;
while let Some(range) = range_in_brackets(&arg, search_index) {
ex_fmt.push_str(&arg[search_index..range.start]);
search_index = range.end;
let expr = &arg[range];
ex_args.push(',');
if let Some(modifier) = mod_regex.find(expr) {
let fmt_spec = modifier.as_str();
if fmt_spec.ends_with('?') {
ex_fmt.push_str(fmt_spec);
} else {
ex_fmt.push_str(&fmt_spec[..(fmt_spec.len() - 1)]);
}
ex_args.push_str(&expr[modifier.end()..]);
} else {
ex_args.push_str(expr);
}
}
ex_fmt.push_str(&arg[search_index..]);
format!("{}!({}{})", func, ex_fmt, ex_args)
}
type Iter<'a> = CharIndices<'a>;
fn range_in_brackets(arg: &str, start_index: usize) -> Option<Range<usize>> {
if let Some(range) = range_of_expression(&mut arg[start_index..].char_indices()) {
return Some((range.start + start_index)..(range.end + start_index));
}
None
}
fn range_of_expression(iter: &mut Iter) -> Option<Range<usize>> {
while let Some((i, c)) = iter.next() {
if c == '{' {
if let Some(end) = find_expr_end(iter) {
return Some((i + 1)..end);
}
}
}
None
}
fn find_expr_end(iter: &mut Iter) -> Option<usize> {
let mut prev_c = '{';
let mut depth = 1;
while let Some((i, c)) = iter.next() {
prev_c = match c {
'{' => {
if depth == 1 && prev_c == '{' {
return None;
} else {
depth += 1;
0 as char
}
}
'}' => {
depth -= 1;
if depth == 0 {
return Some(i);
}
0 as char
}
'r' => try_raw_string(iter, prev_c),
'"' => do_string(iter),
'/' => try_line_comment(iter, prev_c),
'*' => try_block_comment(iter, prev_c),
'\'' => try_char(iter),
_ => c,
};
}
None
}
fn try_raw_string(iter: &mut Iter, prev_c: char) -> char {
if prev_c.is_ascii_alphanumeric() || prev_c == '_' {
return 'r';
}
let mut depth = 0;
let mut iter_copy = iter.clone();
let mut prev_c = 'r';
while let Some((_, c)) = iter_copy.next() {
if c == '"' {
iter.next();
do_raw_string(iter, depth);
break;
} else if c == '#' {
iter.next();
depth += 1;
} else {
return prev_c;
}
prev_c = c;
}
0 as char
}
fn do_raw_string(iter: &mut Iter, depth: usize) {
let mut depth_counter = depth;
let mut prev_c = 0 as char;
for (_, c) in iter {
if c == '"' && depth == 0 {
return;
} else if c == '#' {
if prev_c == '"' || depth_counter != depth {
depth_counter -= 1;
if depth_counter == 0 {
return;
}
}
} else if depth_counter != depth {
depth_counter = depth;
}
prev_c = c;
}
}
fn do_string(iter: &mut Iter) -> char {
let mut prev_c = 0 as char;
for (_, c) in iter {
if c == '"' && prev_c != '\\' {
break;
}
prev_c = c;
}
return 0 as char;
}
fn try_char(iter: &mut Iter) -> char {
if let Some((_, c)) = iter.next() {
if c.is_ascii_alphabetic() || c == '_' {
let iter_copy = iter.clone();
for (_, c) in iter_copy {
if !(c.is_ascii_alphanumeric() || c == '_') {
if c == '\'' {
iter.next();
}
return 0 as char;
}
iter.next();
}
} else {
let mut prev_c = c;
for (_, c) in iter {
if c == '\'' && prev_c != '\\' {
return 0 as char;
}
prev_c = c;
}
}
}
0 as char
}
fn try_line_comment(iter: &mut Iter, prev_c: char) -> char {
if prev_c == '/' {
for (_, c) in iter {
if c == '\n' {
break;
}
}
return 0 as char;
}
'/'
}
fn try_block_comment(iter: &mut Iter, prev_c: char) -> char {
if prev_c == '/' {
let mut prev_c = 0 as char;
let mut depth = 1;
for (_, c) in iter {
if prev_c == '/' && c == '*' {
depth += 1;
prev_c = 0 as char;
} else if prev_c == '*' && c == '/' {
depth -= 1;
if depth == 0 {
break;
}
prev_c = 0 as char;
} else {
prev_c = c;
}
}
return 0 as char;
}
'*'
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_unbalanced_bracket_in_string_with_escape() {
test_helper(r#""lorem {"{\"{"} ipsum""#, r#""lorem {} ipsum","{\"{""#);
}
#[test]
fn test_unbalanced_bracket_in_char() {
test_helper(r#""lorem {'{'} ipsum""#, r#""lorem {} ipsum",'{'"#);
}
#[test]
fn test_unbalanced_bracket_in_string() {
test_helper(
r#""lorem {"{dolor"} ipsum""#,
r#""lorem {} ipsum","{dolor""#,
);
}
#[test]
fn test_struct_create() {
test_helper(
r#""lorem {Dolor{sit:"amet"}} ipsum""#,
r#""lorem {} ipsum",Dolor{sit:"amet"}"#,
);
}
#[test]
fn test_literal_string() {
test_helper(
r#""lorem {"dolor sit amet"} ipsum""#,
r#""lorem {} ipsum","dolor sit amet""#,
);
}
#[test]
fn test_consecutive_formats() {
test_helper(r#""{lorem}{ipsum}""#, r#""{}{}",lorem,ipsum"#);
}
#[test]
fn test_escaped_open_brackets() {
test_helper(r#""lorem {{ ipsum""#, r#""lorem {{ ipsum""#);
}
#[test]
fn test_escaped_close_brackets() {
test_helper(r#""lorem }} ipsum""#, r#""lorem }} ipsum""#);
}
#[test]
fn test_escaped_brackets() {
test_helper(r#""{{}}""#, r#""{{}}""#);
}
#[test]
fn test_no_format() {
test_helper(r#""lorem ipsum""#, r#""lorem ipsum""#);
}
#[test]
fn test_no_arg() {
test_helper("", "");
}
#[test]
fn test_empty_str_arg() {
test_helper(r#""""#, r#""""#);
}
#[test]
fn test_empty_marker() {
test_helper(r#""lorem {} ipsum""#, r#""lorem {} ipsum","#);
}
#[test]
fn test_literal_number() {
test_helper(r#""lorem {123} ipsum""#, r#""lorem {} ipsum",123"#);
}
#[test]
fn test_one_argument() {
test_helper(r#""lorem {dolor} ipsum""#, r#""lorem {} ipsum",dolor"#);
}
#[test]
fn test_multiple_same_argument() {
test_helper(
r#""lorem {dolor} ipsum {dolor} sit {dolor} amet""#,
r#""lorem {} ipsum {} sit {} amet",dolor,dolor,dolor"#,
);
}
#[test]
fn test_multiple_arguments() {
test_helper(
r#""lorem {ipsum} dolor {sit} amet, {consectetur} adipiscing""#,
r#""lorem {} dolor {} amet, {} adipiscing",ipsum,sit,consectetur"#,
);
}
#[test]
fn test_nested_argument() {
test_helper(
r#""lorem {dolor.sit} ipsum""#,
r#""lorem {} ipsum",dolor.sit"#,
);
}
#[test]
fn test_array_argument() {
test_helper(
r#""lorem {dolor[0]} ipsum""#,
r#""lorem {} ipsum",dolor[0]"#,
);
}
#[test]
fn test_function_argument() {
test_helper(
r#""lorem {dolor(arg)} ipsum""#,
r#""lorem {} ipsum",dolor(arg)"#,
);
}
#[test]
fn test_debug_argument() {
test_helper(r#""lorem {:?dolor} ipsum""#, r#""lorem {:?} ipsum",dolor"#);
}
#[test]
fn test_mixed_empty_argument() {
test_helper(
r#""lorem {dolor} ipsum {} sit""#,
r#""lorem {} ipsum {} sit",dolor,"#,
);
}
#[test]
fn test_quote_outside_expression() {
test_helper(r#""lorem "{"ipsum"}"""#, r#""lorem "{}"","ipsum""#);
}
#[test]
fn test_raw_string() {
test_helper(
r###""{r##""{lorem}"#"{""##} {ipsum}""###,
r###""{} {}",r##""{lorem}"#"{""##,ipsum"###,
);
}
#[test]
fn test_line_comment() {
test_helper(
r#""{ {// lorem ipsum { "
"dolor"} }""#,
r#""{}", {// lorem ipsum { "
"dolor"} "#,
);
}
#[test]
fn test_block_comment() {
test_helper(r#""{/*lorem { ipsum*/10}""#, r#""{}",/*lorem { ipsum*/10"#);
}
#[test]
fn test_nested_block_comment() {
test_helper(
r#""lorem {/*/*inside comment*/still inside comment*/"ipsum"}""#,
r#""lorem {}",/*/*inside comment*/still inside comment*/"ipsum""#,
);
}
#[test]
fn test_format_width() {
test_helper(r#""{:04 42}""#, r#""{:04}",42"#);
}
fn test_helper(in_arg: &str, out_arg: &str) {
let expected = format!("format!({})", out_arg);
assert_eq!(ex_impl("format", in_arg), expected);
}
}